/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development * and Distribution License("CDDL") (collectively, the "License"). You * may not use this file except in compliance with the License. You can obtain * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html * or glassfish/bootstrap/legal/LICENSE.txt. See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt. * Sun designates this particular file as subject to the "Classpath" exception * as provided by Sun in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the License * Header, with the fields enclosed by brackets [] replaced by your own * identifying information: "Portions Copyrighted [year] * [name of copyright owner]" * * Contributor(s): * * If you wish your version of this file to be governed by only the CDDL or * only the GPL Version 2, indicate your decision by adding "[Contributor] * elects to include this software in this distribution under the [CDDL or GPL * Version 2] license." If you don't indicate a single choice of license, a * recipient has the option to distribute your version of this file under * either the CDDL, the GPL Version 2 or to extend the choice of license to * its licensees as provided above. However, if you add GPL Version 2 code * and therefore, elected the GPL Version 2 license, then the option applies * only if the new code is made subject to such option by the copyright * holder. */ /* * @(#)SMTPTransport.java 1.89 07/07/03 */ package com.sun.mail.smtp; import java.io.*; import java.net.*; import java.util.*; import javax.mail.*; import javax.mail.event.*; import javax.mail.internet.*; import com.sun.mail.util.*; /** * This class implements the Transport abstract class using SMTP for * message submission and transport. <p> * * See the <a href="package-summary.html">com.sun.mail.smtp</a> package * documentation for further information on the SMTP protocol provider. <p> * * This class includes many protected methods that allow a subclass to * extend this class and add support for non-standard SMTP commands. * The {@link #issueCommand} and {@link #sendCommand} methods can be * used to send simple SMTP commands. Other methods such as the * {@link #mailFrom} and {@link #data} methods can be overridden to * insert new commands before or after the corresponding SMTP commands. * For example, a subclass could do this to send the XACT command * before sending the DATA command: * <pre> * protected OutputStream data() throws MessagingException { * if (supportsExtension("XACCOUNTING")) * issueCommand("XACT", 25); * return super.data(); * } * </pre> * * @author Max Spivak * @author Bill Shannon * @author Dean Gibson (DIGEST-MD5 authentication) * @version 1.89, 07/07/03 * * @see javax.mail.event.ConnectionEvent * @see javax.mail.event.TransportEvent */ public class SMTPTransport extends Transport { private String name = "smtp"; // Name of this protocol private int defaultPort = 25; // default SMTP port private boolean isSSL = false; // use SSL? // Following fields valid only during the sendMessage method. private MimeMessage message; // Message to be sent private Address[] addresses; // Addresses to which to send the msg // Valid sent, valid unsent and invalid addresses private Address[] validSentAddr, validUnsentAddr, invalidAddr; // Did we send the message even though some addresses were invalid? private boolean sendPartiallyFailed = false; // If so, here's an exception we need to throw private MessagingException exception; // stream where message data is written private SMTPOutputStream dataStream; // Map of SMTP service extensions supported by server, if EHLO used. private Hashtable extMap; private boolean quitWait = false; // true if we should wait private String saslRealm = UNKNOWN; private boolean reportSuccess; // throw an exception even on success private boolean useStartTLS; // use STARTTLS command private boolean useRset; // use RSET instead of NOOP private PrintStream out; // debug output stream private String localHostName; // our own host name private String lastServerResponse; // last SMTP response private int lastReturnCode; // last SMTP return code /** Headers that should not be included when sending */ private static final String[] ignoreList = { "Bcc", "Content-Length" }; private static final byte[] CRLF = { (byte)'\r', (byte)'\n' }; private static final String UNKNOWN = "UNKNOWN"; // place holder /** * Constructor that takes a Session object and a URLName * that represents a specific SMTP server. */ public SMTPTransport(Session session, URLName urlname) { this(session, urlname, "smtp", 25, false); } /** * Constructor used by this class and by SMTPSSLTransport subclass. */ protected SMTPTransport(Session session, URLName urlname, String name, int defaultPort, boolean isSSL) { super(session, urlname); if (urlname != null) name = urlname.getProtocol(); this.name = name; this.defaultPort = defaultPort; this.isSSL = isSSL; out = session.getDebugOut(); // setting mail.smtp.quitwait to false causes us to not wait for the // response from the QUIT command String s = session.getProperty("mail." + name + ".quitwait"); quitWait = s == null || s.equalsIgnoreCase("true"); // mail.smtp.reportsuccess causes us to throw an exception on success s = session.getProperty("mail." + name + ".reportsuccess"); reportSuccess = s != null && s.equalsIgnoreCase("true"); // mail.smtp.starttls.enable enables use of STARTTLS command s = session.getProperty("mail." + name + ".starttls.enable"); useStartTLS = s != null && s.equalsIgnoreCase("true"); // mail.smtp.userset causes us to use RSET instead of NOOP // for isConnected s = session.getProperty("mail." + name + ".userset"); useRset = s != null && s.equalsIgnoreCase("true"); } /** * Get the name of the local host, for use in the EHLO and HELO commands. * The property mail.smtp.localhost overrides mail.smtp.localaddress, * which overrides what InetAddress would tell us. */ public synchronized String getLocalHost() { try { // get our hostname and cache it for future use if (localHostName == null || localHostName.length() <= 0) localHostName = session.getProperty("mail." + name + ".localhost"); if (localHostName == null || localHostName.length() <= 0) localHostName = session.getProperty("mail." + name + ".localaddress"); if (localHostName == null || localHostName.length() <= 0) { InetAddress localHost = InetAddress.getLocalHost(); localHostName = localHost.getHostName(); // if we can't get our name, use local address literal if (localHostName == null) // XXX - not correct for IPv6 localHostName = "[" + localHost.getHostAddress() + "]"; } } catch (UnknownHostException uhex) { } return localHostName; } /** * Set the name of the local host, for use in the EHLO and HELO commands. * * @since JavaMail 1.3.1 */ public synchronized void setLocalHost(String localhost) { localHostName = localhost; } /** * Start the SMTP protocol on the given socket, which was already * connected by the caller. Useful for implementing the SMTP ATRN * command (RFC 2645) where an existing connection is used when * the server reverses roles and becomes the client. * * @since JavaMail 1.3.3 */ public synchronized void connect(Socket socket) throws MessagingException { serverSocket = socket; super.connect(); } /** * Gets the SASL realm to be used for DIGEST-MD5 authentication. * * @return the name of the realm to use for SASL authentication. * * @since JavaMail 1.3.1 */ public synchronized String getSASLRealm() { if (saslRealm == UNKNOWN) { saslRealm = session.getProperty("mail." + name + ".sasl.realm"); if (saslRealm == null) // try old name saslRealm = session.getProperty("mail." + name + ".saslrealm"); } return saslRealm; } /** * Sets the SASL realm to be used for DIGEST-MD5 authentication. * * @param saslRealm the name of the realm to use for * SASL authentication. * * @since JavaMail 1.3.1 */ public synchronized void setSASLRealm(String saslRealm) { this.saslRealm = saslRealm; } /** * Should we report even successful sends by throwing an exception? * If so, a <code>SendFailedException</code> will always be thrown and * an {@link com.sun.mail.smtp.SMTPAddressSucceededException * SMTPAddressSucceededException} will be included in the exception * chain for each successful address, along with the usual * {@link com.sun.mail.smtp.SMTPAddressFailedException * SMTPAddressFailedException} for each unsuccessful address. * * @return true if an exception will be thrown on successful sends. * * @since JavaMail 1.3.2 */ public synchronized boolean getReportSuccess() { return reportSuccess; } /** * Set whether successful sends should be reported by throwing * an exception. * * @param reportSuccess should we throw an exception on success? * * @since JavaMail 1.3.2 */ public synchronized void setReportSuccess(boolean reportSuccess) { this.reportSuccess = reportSuccess; } /** * Should we use the STARTTLS command to secure the connection * if the server supports it? * * @return true if the STARTTLS command will be used * * @since JavaMail 1.3.2 */ public synchronized boolean getStartTLS() { return useStartTLS; } /** * Set whether the STARTTLS command should be used. * * @param useStartTLS should we use the STARTTLS command? * * @since JavaMail 1.3.2 */ public synchronized void setStartTLS(boolean useStartTLS) { this.useStartTLS = useStartTLS; } /** * Should we use the RSET command instead of the NOOP command * in the @{link #isConnected isConnected} method? * * @return true if RSET will be used * * @since JavaMail 1.4 */ public synchronized boolean getUseRset() { return useRset; } /** * Set whether the RSET command should be used instead of the * NOOP command in the @{link #isConnected isConnected} method. * * @param useRset should we use the RSET command? * * @since JavaMail 1.4 */ public synchronized void setUseRset(boolean useRset) { this.useRset = useRset; } /** * Return the last response we got from the server. * A failed send is often followed by an RSET command, * but the response from the RSET command is not saved. * Instead, this returns the response from the command * before the RSET command. * * @return last response from server * * @since JavaMail 1.3.2 */ public synchronized String getLastServerResponse() { return lastServerResponse; } /** * Return the return code from the last response we got from the server. * * @return return code from last response from server * * @since JavaMail 1.4.1 */ public synchronized int getLastReturnCode() { return lastReturnCode; } private DigestMD5 md5support; private synchronized DigestMD5 getMD5() { if (md5support == null) md5support = new DigestMD5(debug ? out : null); return md5support; } /** * Performs the actual protocol-specific connection attempt. * Will attempt to connect to "localhost" if the host was null. <p> * * Unless mail.smtp.ehlo is set to false, we'll try to identify * ourselves using the ESMTP command EHLO. * * If mail.smtp.auth is set to true, we insist on having a username * and password, and will try to authenticate ourselves if the server * supports the AUTH extension (RFC 2554). * * @param host the name of the host to connect to * @param port the port to use (-1 means use default port) * @param user the name of the user to login as * @param passwd the user's password * @return true if connection successful, false if authentication failed * @exception MessagingException for non-authentication failures */ protected boolean protocolConnect(String host, int port, String user, String passwd) throws MessagingException { // setting mail.smtp.ehlo to false disables attempts to use EHLO String ehloStr = session.getProperty("mail." + name + ".ehlo"); boolean useEhlo = ehloStr == null || !ehloStr.equalsIgnoreCase("false"); // setting mail.smtp.auth to true enables attempts to use AUTH String authStr = session.getProperty("mail." + name + ".auth"); boolean useAuth = authStr != null && authStr.equalsIgnoreCase("true"); DigestMD5 md5; if (debug) out.println("DEBUG SMTP: useEhlo " + useEhlo + ", useAuth " + useAuth); /* * If mail.smtp.auth is set, make sure we have a valid username * and password, even if we might not end up using it (e.g., * because the server doesn't support ESMTP or doesn't support * the AUTH extension). */ if (useAuth && (user == null || passwd == null)) return false; /* * If port is not specified, set it to value of mail.smtp.port * property if it exists, otherwise default to 25. */ if (port == -1) { String portstring = session.getProperty("mail." + name + ".port"); if (portstring != null) { port = Integer.parseInt(portstring); } else { port = defaultPort; } } if (host == null || host.length() == 0) host = "localhost"; boolean succeed = false; if (serverSocket != null) openServer(); // only happens from connect(socket) else openServer(host, port); if (useEhlo) succeed = ehlo(getLocalHost()); if (!succeed) helo(getLocalHost()); if (useStartTLS && supportsExtension("STARTTLS")) { startTLS(); /* * Have to issue another EHLO to update list of extensions * supported, especially authentication mechanisms. * Don't know if this could ever fail, but we ignore failure. */ ehlo(getLocalHost()); } if ((useAuth || (user != null && passwd != null)) && (supportsExtension("AUTH") || supportsExtension("AUTH=LOGIN"))) { if (debug) { out.println("DEBUG SMTP: Attempt to authenticate"); if (!supportsAuthentication("LOGIN") && supportsExtension("AUTH=LOGIN")) out.println("DEBUG SMTP: use AUTH=LOGIN hack"); } // if authentication fails, close connection and return false if (supportsAuthentication("LOGIN") || supportsExtension("AUTH=LOGIN")) { // XXX - could use "initial response" capability int resp = simpleCommand("AUTH LOGIN"); /* * A 530 response indicates that the server wants us to * issue a STARTTLS command first. Do that and try again. */ if (resp == 530) { startTLS(); resp = simpleCommand("AUTH LOGIN"); } /* * Wrap a BASE64Encoder around a ByteArrayOutputstream * to craft b64 encoded username and password strings. * * Also note that unlike the B64 definition in MIME, CRLFs * should *not* be inserted during the encoding process. * So I use Integer.MAX_VALUE (0x7fffffff (> 1G)) as the * bytesPerLine, which should be sufficiently large! */ try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); OutputStream b64os = new BASE64EncoderStream(bos, Integer.MAX_VALUE); if (resp == 334) { // obtain b64 encoded bytes b64os.write(ASCIIUtility.getBytes(user)); b64os.flush(); // complete the encoding // send username resp = simpleCommand(bos.toByteArray()); bos.reset(); // reset buffer } if (resp == 334) { // obtain b64 encoded bytes b64os.write(ASCIIUtility.getBytes(passwd)); b64os.flush(); // complete the encoding // send passwd resp = simpleCommand(bos.toByteArray()); bos.reset(); // reset buffer } } catch (IOException ex) { // should never happen, ignore } finally { if (resp != 235) { closeConnection(); return false; } } } else if (supportsAuthentication("PLAIN")) { // XXX - could use "initial response" capability int resp = simpleCommand("AUTH PLAIN"); try { ByteArrayOutputStream bos = new ByteArrayOutputStream(); OutputStream b64os = new BASE64EncoderStream(bos, Integer.MAX_VALUE); if (resp == 334) { // send "<NUL>user<NUL>passwd" // XXX - we don't send an authorization identity b64os.write(0); b64os.write(ASCIIUtility.getBytes(user)); b64os.write(0); b64os.write(ASCIIUtility.getBytes(passwd)); b64os.flush(); // complete the encoding // send username resp = simpleCommand(bos.toByteArray()); } } catch (IOException ex) { // should never happen, ignore } finally { if (resp != 235) { closeConnection(); return false; } } } else if (supportsAuthentication("DIGEST-MD5") && (md5 = getMD5()) != null) { int resp = simpleCommand("AUTH DIGEST-MD5"); try { if (resp == 334) { byte[] b = md5.authClient(host, user, passwd, getSASLRealm(), lastServerResponse); resp = simpleCommand(b); if (resp == 334) { // client authenticated by server if (!md5.authServer(lastServerResponse)) { // server NOT authenticated by client !!! resp = -1; } else { // send null response resp = simpleCommand(new byte[0]); } } } } catch (Exception ex) { // should never happen, ignore if (debug) out.println("DEBUG SMTP: DIGEST-MD5: " + ex); } finally { if (resp != 235) { closeConnection(); return false; } } } } // we connected correctly return true; } /** * Send the Message to the specified list of addresses.<p> * * If all the <code>addresses</code> succeed the SMTP check * using the <code>RCPT TO:</code> command, we attempt to send the message. * A TransportEvent of type MESSAGE_DELIVERED is fired indicating the * successful submission of a message to the SMTP host.<p> * * If some of the <code>addresses</code> fail the SMTP check, * and the <code>mail.stmp.sendpartial</code> property is not set, * sending is aborted. The TransportEvent of type MESSAGE_NOT_DELIVERED * is fired containing the valid and invalid addresses. The * SendFailedException is also thrown. <p> * * If some of the <code>addresses</code> fail the SMTP check, * and the <code>mail.stmp.sendpartial</code> property is set to true, * the message is sent. The TransportEvent of type * MESSAGE_PARTIALLY_DELIVERED * is fired containing the valid and invalid addresses. The * SMTPSendFailedException is also thrown. <p> * * MessagingException is thrown if the message can't write out * an RFC822-compliant stream using its <code>writeTo</code> method. <p> * * @param message The MimeMessage to be sent * @param addresses List of addresses to send this message to * @see javax.mail.event.TransportEvent * @exception SMTPSendFailedException if the send failed because of * an SMTP command error * @exception SendFailedException if the send failed because of * invalid addresses. * @exception MessagingException if the connection is dead * or not in the connected state or if the message is * not a MimeMessage. */ public synchronized void sendMessage(Message message, Address[] addresses) throws MessagingException, SendFailedException { checkConnected(); // check if the message is a valid MIME/RFC822 message and that // it has all valid InternetAddresses; fail if not if (!(message instanceof MimeMessage)) { if (debug) out.println("DEBUG SMTP: Can only send RFC822 msgs"); throw new MessagingException("SMTP can only send RFC822 messages"); } for (int i = 0; i < addresses.length; i++) { if (!(addresses[i] instanceof InternetAddress)) { throw new MessagingException(addresses[i] + " is not an InternetAddress"); } } this.message = (MimeMessage)message; this.addresses = addresses; validUnsentAddr = addresses; // until we know better expandGroups(); boolean use8bit = false; if (message instanceof SMTPMessage) use8bit = ((SMTPMessage)message).getAllow8bitMIME(); if (!use8bit) { String ebStr = session.getProperty("mail." + name + ".allow8bitmime"); use8bit = ebStr != null && ebStr.equalsIgnoreCase("true"); } if (debug) out.println("DEBUG SMTP: use8bit " + use8bit); if (use8bit && supportsExtension("8BITMIME")) { if (convertTo8Bit(this.message)) { // in case we made any changes, save those changes // XXX - this will change the Message-ID try { this.message.saveChanges(); } catch (MessagingException mex) { // ignore it } } } try { mailFrom(); rcptTo(); this.message.writeTo(data(), ignoreList); finishData(); if (sendPartiallyFailed) { // throw the exception, // fire TransportEvent.MESSAGE_PARTIALLY_DELIVERED event if (debug) out.println("DEBUG SMTP: Sending partially failed " + "because of invalid destination addresses"); notifyTransportListeners( TransportEvent.MESSAGE_PARTIALLY_DELIVERED, validSentAddr, validUnsentAddr, invalidAddr, this.message); throw new SMTPSendFailedException(".", lastReturnCode, lastServerResponse, exception, validSentAddr, validUnsentAddr, invalidAddr); } notifyTransportListeners(TransportEvent.MESSAGE_DELIVERED, validSentAddr, validUnsentAddr, invalidAddr, this.message); } catch (MessagingException mex) { if (debug) mex.printStackTrace(out); notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, validSentAddr, validUnsentAddr, invalidAddr, this.message); throw mex; } catch (IOException ex) { if (debug) ex.printStackTrace(out); // if we catch an IOException, it means that we want // to drop the connection so that the message isn't sent try { closeConnection(); } catch (MessagingException mex) { /* ignore it */ } notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, validSentAddr, validUnsentAddr, invalidAddr, this.message); throw new MessagingException("IOException while sending message", ex); } finally { // no reason to keep this data around validSentAddr = validUnsentAddr = invalidAddr = null; this.addresses = null; this.message = null; this.exception = null; sendPartiallyFailed = false; } } /** * Close the Transport and terminate the connection to the server. */ public synchronized void close() throws MessagingException { if (!super.isConnected()) // Already closed. return; try { if (serverSocket != null) { sendCommand("QUIT"); if (quitWait) { int resp = readServerResponse(); if (resp != 221 && resp != -1) out.println("DEBUG SMTP: QUIT failed with " + resp); } } } finally { closeConnection(); } } private void closeConnection() throws MessagingException { try { if (serverSocket != null) serverSocket.close(); } catch (IOException ioex) { // shouldn't happen throw new MessagingException("Server Close Failed", ioex); } finally { serverSocket = null; serverOutput = null; serverInput = null; lineInputStream = null; if (super.isConnected()) // only notify if already connected super.close(); } } /** * Check whether the transport is connected. Override superclass * method, to actually ping our server connection. */ public synchronized boolean isConnected() { if (!super.isConnected()) // if we haven't been connected at all, don't bother with NOOP return false; try { // sendmail may respond slowly to NOOP after many requests // so if mail.smtp.userset is set we use RSET instead of NOOP. if (useRset) sendCommand("RSET"); else sendCommand("NOOP"); int resp = readServerResponse(); // NOOP should return 250 on success, however, SIMS 3.2 returns // 200, so we work around it. // // Hotmail doesn't implement the NOOP command at all so assume // any kind of response means we're still connected. // That is, any response except 421, which means the server // is shutting down the connection. // if (resp >= 0 && resp != 421) { return true; } else { try { closeConnection(); } catch (MessagingException mex) { } // ignore it return false; } } catch (Exception ex) { try { closeConnection(); } catch (MessagingException mex) { } // ignore it return false; } } /** * Expand any group addresses. */ private void expandGroups() { Vector groups = null; for (int i = 0; i < addresses.length; i++) { InternetAddress a = (InternetAddress)addresses[i]; if (a.isGroup()) { if (groups == null) { // first group, catch up with where we are groups = new Vector(); for (int k = 0; k < i; k++) groups.addElement(addresses[k]); } // parse it and add each individual address try { InternetAddress[] ia = a.getGroup(true); if (ia != null) { for (int j = 0; j < ia.length; j++) groups.addElement(ia[j]); } else groups.addElement(a); } catch (ParseException pex) { // parse failed, add the whole thing groups.addElement(a); } } else { // if we've started accumulating a list, add this to it if (groups != null) groups.addElement(a); } } // if we have a new list, convert it back to an array if (groups != null) { InternetAddress[] newa = new InternetAddress[groups.size()]; groups.copyInto(newa); addresses = newa; } } /** * If the Part is a text part and has a Content-Transfer-Encoding * of "quoted-printable" or "base64", and it obeys the rules for * "8bit" encoding, change the encoding to "8bit". If the part is * a multipart, recursively process all its parts. * * @return true if any changes were made * * XXX - This is really quite a hack. */ private boolean convertTo8Bit(MimePart part) { boolean changed = false; try { if (part.isMimeType("text/*")) { String enc = part.getEncoding(); if (enc != null && (enc.equalsIgnoreCase("quoted-printable") || enc.equalsIgnoreCase("base64"))) { InputStream is = part.getInputStream(); if (is8Bit(is)) { /* * If the message was created using an InputStream * then we have to extract the content as an object * and set it back as an object so that the content * will be re-encoded. * * If the message was not created using an InputStream, * the following should have no effect. */ part.setContent(part.getContent(), part.getContentType()); part.setHeader("Content-Transfer-Encoding", "8bit"); changed = true; } } } else if (part.isMimeType("multipart/*")) { MimeMultipart mp = (MimeMultipart)part.getContent(); int count = mp.getCount(); for (int i = 0; i < count; i++) { if (convertTo8Bit((MimePart)mp.getBodyPart(i))) changed = true; } } } catch (IOException ioex) { // any exception causes us to give up } catch (MessagingException mex) { // any exception causes us to give up } return changed; } /** * Check whether the data in the given InputStream follows the * rules for 8bit text. Lines have to be 998 characters or less * and no NULs are allowed. CR and LF must occur in pairs but we * don't check that because we assume this is text and we convert * all CR/LF combinations into canonical CRLF later. */ private boolean is8Bit(InputStream is) { int b; int linelen = 0; boolean need8bit = false; try { while ((b = is.read()) >= 0) { b &= 0xff; if (b == '\r' || b == '\n') linelen = 0; else if (b == 0) return false; else { linelen++; if (linelen > 998) // 1000 - CRLF return false; } if (b > 0x7f) need8bit = true; } } catch (IOException ex) { return false; } if (debug && need8bit) out.println("DEBUG SMTP: found an 8bit part"); return need8bit; } protected void finalize() throws Throwable { super.finalize(); try { closeConnection(); } catch (MessagingException mex) { } // ignore it } ///////////////////// smtp stuff /////////////////////// private BufferedInputStream serverInput; private LineInputStream lineInputStream; private OutputStream serverOutput; private Socket serverSocket; /////// smtp protocol ////// /** * Issue the <code>HELO</code> command. * * @param domain our domain * * @since JavaMail 1.4.1 */ protected void helo(String domain) throws MessagingException { if (domain != null) issueCommand("HELO " + domain, 250); else issueCommand("HELO", 250); } /** * Issue the <code>EHLO</code> command. * Collect the returned list of service extensions. * * @param domain our domain * @return true if command succeeds * * @since JavaMail 1.4.1 */ protected boolean ehlo(String domain) throws MessagingException { String cmd; if (domain != null) cmd = "EHLO " + domain; else cmd = "EHLO"; sendCommand(cmd); int resp = readServerResponse(); if (resp == 250) { // extract the supported service extensions BufferedReader rd = new BufferedReader(new StringReader(lastServerResponse)); String line; extMap = new Hashtable(); try { boolean first = true; while ((line = rd.readLine()) != null) { if (first) { // skip first line which is the greeting first = false; continue; } if (line.length() < 5) continue; // shouldn't happen line = line.substring(4); // skip response code int i = line.indexOf(' '); String arg = ""; if (i > 0) { arg = line.substring(i + 1); line = line.substring(0, i); } if (debug) out.println("DEBUG SMTP: Found extension \"" + line + "\", arg \"" + arg + "\""); extMap.put(line.toUpperCase(Locale.ENGLISH), arg); } } catch (IOException ex) { } // can't happen } return resp == 250; } /** * Issue the <code>MAIL FROM:</code> command to start sending a message. <p> * * Gets the sender's address in the following order: * <ol> * <li>SMTPMessage.getEnvelopeFrom()</li> * <li>mail.smtp.from property</li> * <li>From: header in the message</li> * <li>System username using the * InternetAddress.getLocalAddress() method</li> * </ol> * * @since JavaMail 1.4.1 */ protected void mailFrom() throws MessagingException { String from = null; if (message instanceof SMTPMessage) from = ((SMTPMessage)message).getEnvelopeFrom(); if (from == null || from.length() <= 0) from = session.getProperty("mail." + name + ".from"); if (from == null || from.length() <= 0) { Address[] fa; Address me; if (message != null && (fa = message.getFrom()) != null && fa.length > 0) me = fa[0]; else me = InternetAddress.getLocalAddress(session); if (me != null) from = ((InternetAddress)me).getAddress(); else throw new MessagingException( "can't determine local email address"); } String cmd = "MAIL FROM:" + normalizeAddress(from); // request delivery status notification? if (supportsExtension("DSN")) { String ret = null; if (message instanceof SMTPMessage) ret = ((SMTPMessage)message).getDSNRet(); if (ret == null) ret = session.getProperty("mail." + name + ".dsn.ret"); // XXX - check for legal syntax? if (ret != null) cmd += " RET=" + ret; } /* * If an RFC 2554 submitter has been specified, and the server * supports the AUTH extension, include the AUTH= element on * the MAIL FROM command. */ if (supportsExtension("AUTH")) { String submitter = null; if (message instanceof SMTPMessage) submitter = ((SMTPMessage)message).getSubmitter(); if (submitter == null) submitter = session.getProperty("mail." + name + ".submitter"); // XXX - check for legal syntax? if (submitter != null) { try { String s = xtext(submitter); cmd += " AUTH=" + s; } catch (IllegalArgumentException ex) { if (debug) out.println("DEBUG SMTP: ignoring invalid submitter: " + submitter + ", Exception: " + ex); } } } /* * Have any extensions to the MAIL command been specified? */ String ext = null; if (message instanceof SMTPMessage) ext = ((SMTPMessage)message).getMailExtension(); if (ext == null) ext = session.getProperty("mail." + name + ".mailextension"); if (ext != null && ext.length() > 0) cmd += " " + ext; issueSendCommand(cmd, 250); } /** * Sends each address to the SMTP host using the <code>RCPT TO:</code> * command and copies the address either into * the validSentAddr or invalidAddr arrays. * Sets the <code>sendFailed</code> * flag to true if any addresses failed. * * @since JavaMail 1.4.1 */ /* * success/failure/error possibilities from the RCPT command * from rfc821, section 4.3 * S: 250, 251 * F: 550, 551, 552, 553, 450, 451, 452 * E: 500, 501, 503, 421 * * and how we map the above error/failure conditions to valid/invalid * address vectors that are reported in the thrown exception: * invalid addr: 550, 501, 503, 551, 553 * valid addr: 552 (quota), 450, 451, 452 (quota), 421 (srvr abort) */ protected void rcptTo() throws MessagingException { Vector valid = new Vector(); Vector validUnsent = new Vector(); Vector invalid = new Vector(); int retCode = -1; MessagingException mex = null; boolean sendFailed = false; MessagingException sfex = null; validSentAddr = validUnsentAddr = invalidAddr = null; boolean sendPartial = false; if (message instanceof SMTPMessage) sendPartial = ((SMTPMessage)message).getSendPartial(); if (!sendPartial) { String sp = session.getProperty("mail." + name + ".sendpartial"); sendPartial = sp != null && sp.equalsIgnoreCase("true"); } if (debug && sendPartial) out.println("DEBUG SMTP: sendPartial set"); boolean dsn = false; String notify = null; if (supportsExtension("DSN")) { if (message instanceof SMTPMessage) notify = ((SMTPMessage)message).getDSNNotify(); if (notify == null) notify = session.getProperty("mail." + name + ".dsn.notify"); // XXX - check for legal syntax? if (notify != null) dsn = true; } // try the addresses one at a time for (int i = 0; i < addresses.length; i++) { sfex = null; InternetAddress ia = (InternetAddress)addresses[i]; String cmd = "RCPT TO:" + normalizeAddress(ia.getAddress()); if (dsn) cmd += " NOTIFY=" + notify; // send the addresses to the SMTP server sendCommand(cmd); // check the server's response for address validity retCode = readServerResponse(); switch (retCode) { case 250: case 251: valid.addElement(ia); if (!reportSuccess) break; // user wants exception even when successful, including // details of the return code // create and chain the exception sfex = new SMTPAddressSucceededException(ia, cmd, retCode, lastServerResponse); if (mex == null) mex = sfex; else mex.setNextException(sfex); break; case 550: case 553: case 503: case 551: case 501: // given address is invalid if (!sendPartial) sendFailed = true; invalid.addElement(ia); // create and chain the exception sfex = new SMTPAddressFailedException(ia, cmd, retCode, lastServerResponse); if (mex == null) mex = sfex; else mex.setNextException(sfex); break; case 552: case 450: case 451: case 452: // given address is valid if (!sendPartial) sendFailed = true; validUnsent.addElement(ia); // create and chain the exception sfex = new SMTPAddressFailedException(ia, cmd, retCode, lastServerResponse); if (mex == null) mex = sfex; else mex.setNextException(sfex); break; default: // handle remaining 4xy & 5xy codes if (retCode >= 400 && retCode <= 499) { // assume address is valid, although we don't really know validUnsent.addElement(ia); } else if (retCode >= 500 && retCode <= 599) { // assume address is invalid, although we don't really know invalid.addElement(ia); } else { // completely unexpected response, just give up if (debug) out.println("DEBUG SMTP: got response code " + retCode + ", with response: " + lastServerResponse); String _lsr = lastServerResponse; // else rset will nuke it int _lrc = lastReturnCode; if (serverSocket != null) // hasn't already been closed issueCommand("RSET", 250); lastServerResponse = _lsr; // restore, for get lastReturnCode = _lrc; throw new SMTPAddressFailedException(ia, cmd, retCode, _lsr); } if (!sendPartial) sendFailed = true; // create and chain the exception sfex = new SMTPAddressFailedException(ia, cmd, retCode, lastServerResponse); if (mex == null) mex = sfex; else mex.setNextException(sfex); break; } } // if we're willing to send to a partial list, and we found no // valid addresses, that's complete failure if (sendPartial && valid.size() == 0) sendFailed = true; // copy the vectors into appropriate arrays if (sendFailed) { // copy invalid addrs invalidAddr = new Address[invalid.size()]; invalid.copyInto(invalidAddr); // copy all valid addresses to validUnsent, since something failed validUnsentAddr = new Address[valid.size() + validUnsent.size()]; int i = 0; for (int j = 0; j < valid.size(); j++) validUnsentAddr[i++] = (Address)valid.elementAt(j); for (int j = 0; j < validUnsent.size(); j++) validUnsentAddr[i++] = (Address)validUnsent.elementAt(j); } else if (reportSuccess || (sendPartial && (invalid.size() > 0 || validUnsent.size() > 0))) { // we'll go on to send the message, but after sending we'll // throw an exception with this exception nested sendPartiallyFailed = true; exception = mex; // copy invalid addrs invalidAddr = new Address[invalid.size()]; invalid.copyInto(invalidAddr); // copy valid unsent addresses to validUnsent validUnsentAddr = new Address[validUnsent.size()]; validUnsent.copyInto(validUnsentAddr); // copy valid addresses to validSent validSentAddr = new Address[valid.size()]; valid.copyInto(validSentAddr); } else { // all addresses pass validSentAddr = addresses; } // print out the debug info if (debug) { if (validSentAddr != null && validSentAddr.length > 0) { out.println("DEBUG SMTP: Verified Addresses"); for (int l = 0; l < validSentAddr.length; l++) { out.println("DEBUG SMTP: " + validSentAddr[l]); } } if (validUnsentAddr != null && validUnsentAddr.length > 0) { out.println("DEBUG SMTP: Valid Unsent Addresses"); for (int j = 0; j < validUnsentAddr.length; j++) { out.println("DEBUG SMTP: " + validUnsentAddr[j]); } } if (invalidAddr != null && invalidAddr.length > 0) { out.println("DEBUG SMTP: Invalid Addresses"); for (int k = 0; k < invalidAddr.length; k++) { out.println("DEBUG SMTP: " + invalidAddr[k]); } } } // throw the exception, fire TransportEvent.MESSAGE_NOT_DELIVERED event if (sendFailed) { if (debug) out.println("DEBUG SMTP: Sending failed " + "because of invalid destination addresses"); notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, validSentAddr, validUnsentAddr, invalidAddr, this.message); // reset the connection so more sends are allowed String lsr = lastServerResponse; // save, for get int lrc = lastReturnCode; try { if (serverSocket != null) issueCommand("RSET", 250); } catch (MessagingException ex) { // if can't reset, best to close the connection try { close(); } catch (MessagingException ex2) { // thrown by close()--ignore, will close() later anyway if (debug) ex2.printStackTrace(out); } } finally { lastServerResponse = lsr; // restore lastReturnCode = lrc; } throw new SendFailedException("Invalid Addresses", mex, validSentAddr, validUnsentAddr, invalidAddr); } } /** * Send the <code>DATA</code> command to the SMTP host and return * an OutputStream to which the data is to be written. * * @since JavaMail 1.4.1 */ protected OutputStream data() throws MessagingException { assert Thread.holdsLock(this); issueSendCommand("DATA", 354); dataStream = new SMTPOutputStream(serverOutput); return dataStream; } /** * Terminate the sent data. * * @since JavaMail 1.4.1 */ protected void finishData() throws IOException, MessagingException { assert Thread.holdsLock(this); dataStream.ensureAtBOL(); issueSendCommand(".", 250); } /** * Issue the <code>STARTTLS</code> command and switch the socket to * TLS mode if it succeeds. * * @since JavaMail 1.4.1 */ protected void startTLS() throws MessagingException { issueCommand("STARTTLS", 220); // it worked, now switch the socket into TLS mode try { serverSocket = SocketFetcher.startTLS(serverSocket, session.getProperties(), "mail." + name); initStreams(); } catch (IOException ioex) { closeConnection(); throw new MessagingException("Could not convert socket to TLS", ioex); } } /////// primitives /////// /** * Connect to server on port and start the SMTP protocol. */ private void openServer(String server, int port) throws MessagingException { if (debug) out.println("DEBUG SMTP: trying to connect to host \"" + server + "\", port " + port + ", isSSL " + isSSL); try { Properties props = session.getProperties(); serverSocket = SocketFetcher.getSocket(server, port, props, "mail." + name, isSSL); // socket factory may've chosen a different port, // update it for the debug messages that follow port = serverSocket.getPort(); initStreams(); int r = -1; if ((r = readServerResponse()) != 220) { serverSocket.close(); serverSocket = null; serverOutput = null; serverInput = null; lineInputStream = null; if (debug) out.println("DEBUG SMTP: could not connect to host \"" + server + "\", port: " + port + ", response: " + r + "\n"); throw new MessagingException( "Could not connect to SMTP host: " + server + ", port: " + port + ", response: " + r); } else { if (debug) out.println("DEBUG SMTP: connected to host \"" + server + "\", port: " + port + "\n"); } } catch (UnknownHostException uhex) { throw new MessagingException("Unknown SMTP host: " + server, uhex); } catch (IOException ioe) { throw new MessagingException("Could not connect to SMTP host: " + server + ", port: " + port, ioe); } } /** * Start the protocol to the server on serverSocket, * assumed to be provided and connected by the caller. */ private void openServer() throws MessagingException { int port = -1; String server = "UNKNOWN"; try { port = serverSocket.getPort(); server = serverSocket.getInetAddress().getHostName(); if (debug) out.println("DEBUG SMTP: starting protocol to host \"" + server + "\", port " + port); initStreams(); int r = -1; if ((r = readServerResponse()) != 220) { serverSocket.close(); serverSocket = null; serverOutput = null; serverInput = null; lineInputStream = null; if (debug) out.println("DEBUG SMTP: got bad greeting from host \"" + server + "\", port: " + port + ", response: " + r + "\n"); throw new MessagingException( "Got bad greeting from SMTP host: " + server + ", port: " + port + ", response: " + r); } else { if (debug) out.println("DEBUG SMTP: protocol started to host \"" + server + "\", port: " + port + "\n"); } } catch (IOException ioe) { throw new MessagingException( "Could not start protocol to SMTP host: " + server + ", port: " + port, ioe); } } private void initStreams() throws IOException { Properties props = session.getProperties(); PrintStream out = session.getDebugOut(); boolean debug = session.getDebug(); String s = props.getProperty("mail.debug.quote"); boolean quote = s != null && s.equalsIgnoreCase("true"); TraceInputStream traceInput = new TraceInputStream(serverSocket.getInputStream(), out); traceInput.setTrace(debug); traceInput.setQuote(quote); TraceOutputStream traceOutput = new TraceOutputStream(serverSocket.getOutputStream(), out); traceOutput.setTrace(debug); traceOutput.setQuote(quote); serverOutput = new BufferedOutputStream(traceOutput); serverInput = new BufferedInputStream(traceInput); lineInputStream = new LineInputStream(serverInput); } /** * Send the command to the server. If the expected response code * is not received, throw a MessagingException. * * @param cmd the command to send * @param expect the expected response code * * @since JavaMail 1.4.1 */ public synchronized void issueCommand(String cmd, int expect) throws MessagingException { sendCommand(cmd); // if server responded with an unexpected return code, // throw the exception, notifying the client of the response if (readServerResponse() != expect) throw new MessagingException(lastServerResponse); } /** * Issue a command that's part of sending a message. */ private void issueSendCommand(String cmd, int expect) throws MessagingException { sendCommand(cmd); // if server responded with an unexpected return code, // throw the exception, notifying the client of the response int ret; if ((ret = readServerResponse()) != expect) { // assume message was not sent to anyone, // combine valid sent & unsent addresses int vsl = validSentAddr == null ? 0 : validSentAddr.length; int vul = validUnsentAddr == null ? 0 : validUnsentAddr.length; Address[] valid = new Address[vsl + vul]; if (vsl > 0) System.arraycopy(validSentAddr, 0, valid, 0, vsl); if (vul > 0) System.arraycopy(validUnsentAddr, 0, valid, vsl, vul); validSentAddr = null; validUnsentAddr = valid; if (debug) out.println("DEBUG SMTP: got response code " + ret + ", with response: " + lastServerResponse); String _lsr = lastServerResponse; // else rset will nuke it int _lrc = lastReturnCode; if (serverSocket != null) // hasn't already been closed issueCommand("RSET", 250); lastServerResponse = _lsr; // restore, for get lastReturnCode = _lrc; throw new SMTPSendFailedException(cmd, ret, lastServerResponse, exception, validSentAddr, validUnsentAddr, invalidAddr); } } /** * Send the command to the server and return the response code * from the server. * * @since JavaMail 1.4.1 */ public synchronized int simpleCommand(String cmd) throws MessagingException { sendCommand(cmd); return readServerResponse(); } /** * Send the command to the server and return the response code * from the server. * * @since JavaMail 1.4.1 */ protected int simpleCommand(byte[] cmd) throws MessagingException { assert Thread.holdsLock(this); sendCommand(cmd); return readServerResponse(); } /** * Sends command <code>cmd</code> to the server terminating * it with <code>CRLF</code>. * * @since JavaMail 1.4.1 */ protected void sendCommand(String cmd) throws MessagingException { sendCommand(ASCIIUtility.getBytes(cmd)); } private void sendCommand(byte[] cmdBytes) throws MessagingException { assert Thread.holdsLock(this); //if (debug) //out.println("DEBUG SMTP SENT: " + new String(cmdBytes, 0)); try { serverOutput.write(cmdBytes); serverOutput.write(CRLF); serverOutput.flush(); } catch (IOException ex) { throw new MessagingException("Can't send command to SMTP host", ex); } } /** * Reads server reponse returning the <code>returnCode</code> * as the number. Returns -1 on failure. Sets * <code>lastServerResponse</code> and <code>lastReturnCode</code>. * * @return server response code * * @since JavaMail 1.4.1 */ protected int readServerResponse() throws MessagingException { assert Thread.holdsLock(this); String serverResponse = ""; int returnCode = 0; StringBuffer buf = new StringBuffer(100); // read the server response line(s) and add them to the buffer // that stores the response try { String line = null; do { line = lineInputStream.readLine(); if (line == null) { serverResponse = buf.toString(); if (serverResponse.length() == 0) serverResponse = "[EOF]"; lastServerResponse = serverResponse; lastReturnCode = -1; if (debug) out.println("DEBUG SMTP: EOF: " + serverResponse); return -1; } buf.append(line); buf.append("\n"); } while (isNotLastLine(line)); serverResponse = buf.toString(); } catch (IOException ioex) { if (debug) out.println("DEBUG SMTP: exception reading response: " + ioex); //ioex.printStackTrace(out); lastServerResponse = ""; lastReturnCode = 0; throw new MessagingException("Exception reading response", ioex); //returnCode = -1; } // print debug info //if (debug) //out.println("DEBUG SMTP RCVD: " + serverResponse); // parse out the return code if (serverResponse != null && serverResponse.length() >= 3) { try { returnCode = Integer.parseInt(serverResponse.substring(0, 3)); } catch (NumberFormatException nfe) { try { close(); } catch (MessagingException mex) { // thrown by close()--ignore, will close() later anyway if (debug) mex.printStackTrace(out); } returnCode = -1; } catch (StringIndexOutOfBoundsException ex) { //if (debug) ex.printStackTrace(out); try { close(); } catch (MessagingException mex) { // thrown by close()--ignore, will close() later anyway if (debug) mex.printStackTrace(out); } returnCode = -1; } } else { returnCode = -1; } if (returnCode == -1 && debug) out.println("DEBUG SMTP: bad server response: " + serverResponse); lastServerResponse = serverResponse; lastReturnCode = returnCode; return returnCode; } /** * Check if we're in the connected state. Don't bother checking * whether the server is still alive, that will be detected later. * * @exception IllegalStateException if not connected * * @since JavaMail 1.4.1 */ protected void checkConnected() { if (!super.isConnected()) throw new IllegalStateException("Not connected"); } // tests if the <code>line</code> is an intermediate line according to SMTP private boolean isNotLastLine(String line) { return line != null && line.length() >= 4 && line.charAt(3) == '-'; } // wraps an address in "<>"'s if necessary private String normalizeAddress(String addr) { if ((!addr.startsWith("<")) && (!addr.endsWith(">"))) return "<" + addr + ">"; else return addr; } /** * Return true if the SMTP server supports the specified service * extension. Extensions are reported as results of the EHLO * command when connecting to the server. See * <A HREF="http://www.ietf.org/rfc/rfc1869.txt">RFC 1869</A> * and other RFCs that define specific extensions. * * @param ext the service extension name * @return true if the extension is supported * * @since JavaMail 1.3.2 */ public boolean supportsExtension(String ext) { return extMap != null && extMap.get(ext.toUpperCase(Locale.ENGLISH)) != null; } /** * Return the parameter the server provided for the specified * service extension, or null if the extension isn't supported. * * @param ext the service extension name * @return the extension parameter * * @since JavaMail 1.3.2 */ public String getExtensionParameter(String ext) { return extMap == null ? null : (String)extMap.get(ext.toUpperCase(Locale.ENGLISH)); } /** * Does the server we're connected to support the specified * authentication mechanism? Uses the extension information * returned by the server from the EHLO command. * * @param auth the authentication mechanism * @return true if the authentication mechanism is supported * * @since JavaMail 1.4.1 */ protected boolean supportsAuthentication(String auth) { assert Thread.holdsLock(this); if (extMap == null) return false; String a = (String)extMap.get("AUTH"); if (a == null) return false; StringTokenizer st = new StringTokenizer(a); while (st.hasMoreTokens()) { String tok = st.nextToken(); if (tok.equalsIgnoreCase(auth)) return true; } return false; } private static char[] hexchar = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; /** * Convert a string to RFC 1891 xtext format. * * <p><pre> * xtext = *( xchar / hexchar ) * * xchar = any ASCII CHAR between "!" (33) and "~" (126) inclusive, * except for "+" and "=". * * ; "hexchar"s are intended to encode octets that cannot appear * ; as ASCII characters within an esmtp-value. * * hexchar = ASCII "+" immediately followed by two upper case * hexadecimal digits * </pre></p> * * @since JavaMail 1.4.1 */ protected static String xtext(String s) { StringBuffer sb = null; for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c >= 128) // not ASCII throw new IllegalArgumentException( "Non-ASCII character in SMTP submitter: " + s); if (c < '!' || c > '~' || c == '+' || c == '=') { if (sb == null) { sb = new StringBuffer(s.length() + 4); sb.append(s.substring(0, i)); } sb.append('+'); sb.append(hexchar[(((int)c)& 0xf0) >> 4]); sb.append(hexchar[((int)c)& 0x0f]); } else { if (sb != null) sb.append(c); } } return sb != null ? sb.toString() : s; } }