001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *  http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    
020    package org.apache.geronimo.javamail.transport.smtp;
021    
022    import java.io.IOException;
023    import java.io.UnsupportedEncodingException;
024    import java.net.Socket; 
025    import java.util.ArrayList;
026    
027    import javax.mail.Address;
028    import javax.mail.AuthenticationFailedException;
029    import javax.mail.Message;
030    import javax.mail.MessagingException;
031    import javax.mail.Session;
032    import javax.mail.Transport;
033    import javax.mail.URLName;
034    import javax.mail.event.TransportEvent;
035    import javax.mail.internet.InternetAddress;
036    import javax.mail.internet.MimeMessage;
037    import javax.mail.internet.MimeMultipart;     
038    import javax.mail.internet.MimePart;     
039    
040    import org.apache.geronimo.javamail.util.ProtocolProperties; 
041    import org.apache.geronimo.javamail.transport.smtp.SMTPConnection.SendStatus; 
042    
043    /**
044     * Simple implementation of SMTP transport. Just does plain RFC821-ish delivery.
045     * <p/> Supported properties : <p/>
046     * <ul>
047     * <li> mail.host : to set the server to deliver to. Default = localhost</li>
048     * <li> mail.smtp.port : to set the port. Default = 25</li>
049     * <li> mail.smtp.locahost : name to use for HELO/EHLO - default getHostName()</li>
050     * </ul>
051     * <p/> There is no way to indicate failure for a given recipient (it's possible
052     * to have a recipient address rejected). The sun impl throws exceptions even if
053     * others successful), but maybe we do a different way... <p/> TODO : lots.
054     * ESMTP, user/pass, indicate failure, etc...
055     *
056     * @version $Rev: 673649 $ $Date: 2008-07-03 06:37:56 -0400 (Thu, 03 Jul 2008) $
057     */
058    public class SMTPTransport extends Transport {
059        /**
060         * property keys for protocol properties. The actual property name will be
061         * appended with "mail." + protocol + ".", where the protocol is either
062         * "smtp" or "smtps".
063         */
064        protected static final String MAIL_SMTP_DSN_NOTIFY = "dsn.notify";
065        protected static final String MAIL_SMTP_SENDPARTIAL = "sendpartial";
066        protected static final String MAIL_SMTP_EXTENSION = "mailextension";
067        protected static final String DEFAULT_MAIL_HOST = "localhost";
068    
069        protected static final int DEFAULT_MAIL_SMTP_PORT = 25;
070        protected static final int DEFAULT_MAIL_SMTPS_PORT = 465;
071    
072    
073        // do we use SSL for our initial connection?
074        protected boolean sslConnection = false;
075        
076        // our accessor for protocol properties and the holder of 
077        // protocol-specific information 
078        protected ProtocolProperties props; 
079        // our active connection object 
080        protected SMTPConnection connection;
081    
082        // the last response line received from the server.
083        protected SMTPReply lastServerResponse = null;
084    
085        /**
086         * Normal constructor for an SMTPTransport() object. This constructor is
087         * used to build a transport instance for the "smtp" protocol.
088         *
089         * @param session
090         *            The attached session.
091         * @param name
092         *            An optional URLName object containing target information.
093         */
094        public SMTPTransport(Session session, URLName name) {
095            this(session, name, "smtp", DEFAULT_MAIL_SMTP_PORT, false);
096        }
097        
098    
099        /**
100         * Common constructor used by the SMTPTransport and SMTPSTransport classes
101         * to do common initialization of defaults.
102         *
103         * @param session
104         *            The host session instance.
105         * @param name
106         *            The URLName of the target.
107         * @param protocol
108         *            The protocol type (either "smtp" or "smtps". This helps us in
109         *            retrieving protocol-specific session properties.
110         * @param defaultPort
111         *            The default port used by this protocol. For "smtp", this will
112         *            be 25. The default for "smtps" is 465.
113         * @param sslConnection
114         *            Indicates whether an SSL connection should be used to initial
115         *            contact the server. This is different from the STARTTLS
116         *            support, which switches the connection to SSL after the
117         *            initial startup.
118         */
119        protected SMTPTransport(Session session, URLName name, String protocol, int defaultPort, boolean sslConnection) {
120            super(session, name);
121            
122            // create the protocol property holder.  This gives an abstraction over the different 
123            // flavors of the protocol. 
124            props = new ProtocolProperties(session, protocol, sslConnection, defaultPort); 
125            // the connection manages connection for the transport 
126            connection = new SMTPConnection(props); 
127        }
128    
129        
130        /**
131         * Connect to a server using an already created socket. This connection is
132         * just like any other connection, except we will not create a new socket.
133         *
134         * @param socket
135         *            The socket connection to use.
136         */
137        public void connect(Socket socket) throws MessagingException {
138            connection.connect(socket); 
139            super.connect();
140        }
141        
142    
143        /**
144         * Do the protocol connection for an SMTP transport. This handles server
145         * authentication, if possible. Returns false if unable to connect to the
146         * server.
147         *
148         * @param host
149         *            The target host name.
150         * @param port
151         *            The server port number.
152         * @param user
153         *            The authentication user (if any).
154         * @param password
155         *            The server password. Might not be sent directly if more
156         *            sophisticated authentication is used.
157         *
158         * @return true if we were able to connect to the server properly, false for
159         *         any failures.
160         * @exception MessagingException
161         */
162        protected boolean protocolConnect(String host, int port, String username, String password)
163                throws MessagingException {
164            // the connection pool handles all of the details here. 
165            return connection.protocolConnect(host, port, username, password);
166        }
167    
168        /**
169         * Send a message to multiple addressees.
170         *
171         * @param message
172         *            The message we're sending.
173         * @param addresses
174         *            An array of addresses to send to.
175         *
176         * @exception MessagingException
177         */
178        public void sendMessage(Message message, Address[] addresses) throws MessagingException {
179            if (!isConnected()) {
180                throw new IllegalStateException("Not connected");
181            }
182            // don't bother me w/ null messages or no addreses
183            if (message == null) {
184                throw new MessagingException("Null message");
185            }
186    
187            // SMTP only handles instances of MimeMessage, not the more general
188            // message case.
189            if (!(message instanceof MimeMessage)) {
190                throw new MessagingException("SMTP can only send MimeMessages");
191            }
192    
193            // we must have a message list.
194            if (addresses == null || addresses.length == 0) {
195                throw new MessagingException("Null or empty address array");
196            }
197            
198            boolean reportSuccess = getReportSuccess(); 
199            
200            // now see how we're configured for this send operation.
201            boolean partialSends = false;
202    
203            // this can be attached directly to the message.
204            if (message instanceof SMTPMessage) {
205                partialSends = ((SMTPMessage) message).getSendPartial();
206            }
207    
208            // if still false on the message object, check for a property
209            // version also
210            if (!partialSends) {
211                partialSends = props.getBooleanProperty(MAIL_SMTP_SENDPARTIAL, false);
212            }
213    
214            boolean haveGroup = false;
215    
216            // enforce the requirement that all of the targets are InternetAddress
217            // instances.
218            for (int i = 0; i < addresses.length; i++) {
219                if (addresses[i] instanceof InternetAddress) {
220                    // and while we're here, see if we have a groups in the address
221                    // list. If we do, then
222                    // we're going to need to expand these before sending.
223                    if (((InternetAddress) addresses[i]).isGroup()) {
224                        haveGroup = true;
225                    }
226                } else {
227                    throw new MessagingException("Illegal InternetAddress " + addresses[i]);
228                }
229            }
230    
231            // did we find a group? Time to expand this into our full target list.
232            if (haveGroup) {
233                addresses = expandGroups(addresses);
234            }
235    
236            SendStatus[] stats = new SendStatus[addresses.length];
237    
238            // create our lists for notification and exception reporting.
239            Address[] sent = null;
240            Address[] unsent = null;
241            Address[] invalid = null;
242    
243            try {
244                // send sender first. If this failed, send a failure notice of the
245                // event, using the full list of
246                // addresses as the unsent, and nothing for the rest.
247                if (!connection.sendMailFrom(message)) {
248                    unsent = addresses;
249                    sent = new Address[0];
250                    invalid = new Address[0];
251                    // notify of the error.
252                    notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, sent, unsent, invalid, message);
253    
254                    // include the reponse information here.
255                    SMTPReply last = connection.getLastServerResponse(); 
256                    // now send an "uber-exception" to indicate the failure.
257                    throw new SMTPSendFailedException("MAIL FROM", last.getCode(), last.getMessage(), null, sent, unsent,
258                            invalid);
259                }
260    
261                // get the additional notification status, if available 
262                String dsn = getDeliveryStatusNotification(message);
263    
264                // we need to know about any failures once we've gone through the
265                // complete list, so keep a
266                // failure flag.
267                boolean sendFailure = false;
268    
269                // event notifcation requires we send lists of successes and
270                // failures broken down by category.
271                // The categories are:
272                //
273                // 1) addresses successfully processed.
274                // 2) addresses deemed valid, but had a processing failure that
275                // prevented sending.
276                // 3) addressed deemed invalid (basically all other processing
277                // failures).
278                ArrayList sentAddresses = new ArrayList();
279                ArrayList unsentAddresses = new ArrayList();
280                ArrayList invalidAddresses = new ArrayList();
281    
282                // Now we add a MAIL TO record for each recipient. At this point, we
283                // just collect
284                for (int i = 0; i < addresses.length; i++) {
285                    InternetAddress target = (InternetAddress) addresses[i];
286    
287                    // write out the record now.
288                    SendStatus status = connection.sendRcptTo(target, dsn);
289                    stats[i] = status;
290    
291                    switch (status.getStatus()) {
292                        // successfully sent
293                        case SendStatus.SUCCESS:
294                            sentAddresses.add(target);
295                            break;
296    
297                        // we have an invalid address of some sort, or a general sending
298                        // error (which we'll
299                        // interpret as due to an invalid address.
300                        case SendStatus.INVALID_ADDRESS:
301                        case SendStatus.GENERAL_ERROR:
302                            sendFailure = true;
303                            invalidAddresses.add(target);
304                            break;
305    
306                        // good address, but this was a send failure.
307                        case SendStatus.SEND_FAILURE:
308                            sendFailure = true;
309                            unsentAddresses.add(target);
310                            break;
311                        }
312                }
313    
314                // if we had a send failure, then we need to check if we allow
315                // partial sends. If not allowed,
316                // we abort the send operation now.
317                if (sendFailure) {
318                    // if we're not allowing partial successes or we've failed on
319                    // all of the addresses, it's
320                    // time to abort.
321                    if (!partialSends || sentAddresses.isEmpty()) {
322                        // we send along the valid and invalid address lists on the
323                        // notifications and
324                        // exceptions.
325                        // however, since we're aborting the entire send, the
326                        // successes need to become
327                        // members of the failure list.
328                        unsentAddresses.addAll(sentAddresses);
329    
330                        // this one is empty.
331                        sent = new Address[0];
332                        unsent = (Address[]) unsentAddresses.toArray(new Address[0]);
333                        invalid = (Address[]) invalidAddresses.toArray(new Address[0]);
334    
335                        // go reset our connection so we can process additional
336                        // sends.
337                        connection.resetConnection();
338    
339                        // get a list of chained exceptions for all of the failures.
340                        MessagingException failures = generateExceptionChain(stats, false);
341    
342                        // now send an "uber-exception" to indicate the failure.
343                        throw new SMTPSendFailedException("MAIL TO", 0, "Invalid Address", failures, sent, unsent, invalid);
344                    }
345                }
346    
347                try {
348                    // try to send the data
349                    connection.sendData(message);
350                } catch (MessagingException e) {
351                    // If there's an error at this point, this is a complete
352                    // delivery failure.
353                    // we send along the valid and invalid address lists on the
354                    // notifications and
355                    // exceptions.
356                    // however, since we're aborting the entire send, the successes
357                    // need to become
358                    // members of the failure list.
359                    unsentAddresses.addAll(sentAddresses);
360    
361                    // this one is empty.
362                    sent = new Address[0];
363                    unsent = (Address[]) unsentAddresses.toArray(new Address[0]);
364                    invalid = (Address[]) invalidAddresses.toArray(new Address[0]);
365                    // notify of the error.
366                    notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, sent, unsent, invalid, message);
367                    // send a send failure exception.
368                    throw new SMTPSendFailedException("DATA", 0, "Send failure", e, sent, unsent, invalid);
369                }
370    
371                // create our lists for notification and exception reporting from
372                // this point on.
373                sent = (Address[]) sentAddresses.toArray(new Address[0]);
374                unsent = (Address[]) unsentAddresses.toArray(new Address[0]);
375                invalid = (Address[]) invalidAddresses.toArray(new Address[0]);
376    
377                // if sendFailure is true, we had an error during the address phase,
378                // but we had permission to
379                // process this as a partial send operation. Now that the data has
380                // been sent ok, it's time to
381                // report the partial failure.
382                if (sendFailure) {
383                    // notify our listeners of the partial delivery.
384                    notifyTransportListeners(TransportEvent.MESSAGE_PARTIALLY_DELIVERED, sent, unsent, invalid, message);
385    
386                    // get a list of chained exceptions for all of the failures (and
387                    // the successes, if reportSuccess has been
388                    // turned on).
389                    MessagingException failures = generateExceptionChain(stats, reportSuccess);
390    
391                    // now send an "uber-exception" to indicate the failure.
392                    throw new SMTPSendFailedException("MAIL TO", 0, "Invalid Address", failures, sent, unsent, invalid);
393                }
394    
395                // notify our listeners of successful delivery.
396                notifyTransportListeners(TransportEvent.MESSAGE_DELIVERED, sent, unsent, invalid, message);
397    
398                // we've not had any failures, but we've been asked to report
399                // success as an exception. Do
400                // this now.
401                if (reportSuccess) {
402                    // generate the chain of success exceptions (we already know
403                    // there are no failure ones to report).
404                    MessagingException successes = generateExceptionChain(stats, reportSuccess);
405                    if (successes != null) {
406                        throw successes;
407                    }
408                }
409            } catch (SMTPSendFailedException e) {
410                // if this is a send failure, we've already handled
411                // notifications....just rethrow it.
412                throw e;
413            } catch (MessagingException e) {
414                // notify of the error.
415                notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, sent, unsent, invalid, message);
416                throw e;
417            }
418        }
419        
420        
421        /**
422         * Determine what delivery status notification should
423         * be added to the RCPT TO: command. 
424         * 
425         * @param message The message we're sending.
426         * 
427         * @return The string NOTIFY= value to add to the command. 
428         */
429        protected String getDeliveryStatusNotification(Message message) {
430            String dsn = null;
431    
432            // there's an optional notification argument that can be added to
433            // MAIL TO. See if we've been
434            // provided with one.
435    
436            // an SMTPMessage object is the first source
437            if (message instanceof SMTPMessage) {
438                // get the notification options
439                int options = ((SMTPMessage) message).getNotifyOptions();
440    
441                switch (options) {
442                // a zero value indicates nothing is set.
443                case 0:
444                    break;
445    
446                case SMTPMessage.NOTIFY_NEVER:
447                    dsn = "NEVER";
448                    break;
449    
450                case SMTPMessage.NOTIFY_SUCCESS:
451                    dsn = "SUCCESS";
452                    break;
453    
454                case SMTPMessage.NOTIFY_FAILURE:
455                    dsn = "FAILURE";
456                    break;
457    
458                case SMTPMessage.NOTIFY_DELAY:
459                    dsn = "DELAY";
460                    break;
461    
462                // now for combinations...there are few enough combinations here
463                // that we can just handle this in the switch statement rather
464                // than have to
465                // concatentate everything together.
466                case (SMTPMessage.NOTIFY_SUCCESS + SMTPMessage.NOTIFY_FAILURE):
467                    dsn = "SUCCESS,FAILURE";
468                    break;
469    
470                case (SMTPMessage.NOTIFY_SUCCESS + SMTPMessage.NOTIFY_DELAY):
471                    dsn = "SUCCESS,DELAY";
472                    break;
473    
474                case (SMTPMessage.NOTIFY_FAILURE + SMTPMessage.NOTIFY_DELAY):
475                    dsn = "FAILURE,DELAY";
476                    break;
477    
478                case (SMTPMessage.NOTIFY_SUCCESS + SMTPMessage.NOTIFY_FAILURE + SMTPMessage.NOTIFY_DELAY):
479                    dsn = "SUCCESS,FAILURE,DELAY";
480                    break;
481                }
482            }
483    
484            // if still null, grab a property value (yada, yada, yada...)
485            if (dsn == null) {
486                dsn = props.getProperty(MAIL_SMTP_DSN_NOTIFY);
487            }
488            return dsn; 
489        }
490        
491    
492    
493        /**
494         * Close the connection. On completion, we'll be disconnected from the
495         * server and unable to send more data.
496         * 
497         * @exception MessagingException
498         */
499        public void close() throws MessagingException {
500            // This is done to ensure proper event notification.
501            super.close();
502            // NB:  We reuse the connection if asked to reconnect 
503            connection.close();
504        }
505        
506        
507        /**
508         * Turn a series of send status items into a chain of exceptions indicating
509         * the state of each send operation.
510         *
511         * @param stats
512         *            The list of SendStatus items.
513         * @param reportSuccess
514         *            Indicates whether we should include the report success items.
515         *
516         * @return The head of a chained list of MessagingExceptions.
517         */
518        protected MessagingException generateExceptionChain(SendStatus[] stats, boolean reportSuccess) {
519            MessagingException current = null;
520    
521            for (int i = 0; i < stats.length; i++) {
522                SendStatus status = stats[i];
523    
524                if (status != null) {
525                    MessagingException nextException = stats[i].getException(reportSuccess);
526                    // if there's an exception associated with this status, chain it
527                    // up with the rest.
528                    if (nextException != null) {
529                        if (current == null) {
530                            current = nextException;
531                        } else {
532                            current.setNextException(nextException);
533                            current = nextException;
534                        }
535                    }
536                }
537            }
538            return current;
539        }
540    
541        /**
542         * Expand the address list by converting any group addresses into single
543         * address targets.
544         *
545         * @param addresses
546         *            The input array of addresses.
547         *
548         * @return The expanded array of addresses.
549         * @exception MessagingException
550         */
551        protected Address[] expandGroups(Address[] addresses) throws MessagingException {
552            ArrayList expandedAddresses = new ArrayList();
553    
554            // run the list looking for group addresses, and add the full group list
555            // to our targets.
556            for (int i = 0; i < addresses.length; i++) {
557                InternetAddress address = (InternetAddress) addresses[i];
558                // not a group? Just copy over to the other list.
559                if (!address.isGroup()) {
560                    expandedAddresses.add(address);
561                } else {
562                    // get the group address and copy each member of the group into
563                    // the expanded list.
564                    InternetAddress[] groupAddresses = address.getGroup(true);
565                    for (int j = 1; j < groupAddresses.length; j++) {
566                        expandedAddresses.add(groupAddresses[j]);
567                    }
568                }
569            }
570    
571            // convert back into an array.
572            return (Address[]) expandedAddresses.toArray(new Address[0]);
573        }
574        
575    
576        /**
577         * Retrieve the local client host name.
578         *
579         * @return The string version of the local host name.
580         * @exception SMTPTransportException
581         */
582        public String getLocalHost() throws MessagingException {
583            return connection.getLocalHost(); 
584        }
585    
586        
587        /**
588         * Explicitly set the local host information.
589         *
590         * @param localHost
591         *            The new localHost name.
592         */
593        public void setLocalHost(String localHost) {
594            connection.setLocalHost(localHost); 
595        }
596    
597        
598        /**
599         * Return the current reportSuccess property.
600         *
601         * @return The current reportSuccess property.
602         */
603        public boolean getReportSuccess() {
604            return connection.getReportSuccess(); 
605        }
606    
607        /**
608         * Set a new value for the reportSuccess property.
609         *
610         * @param report
611         *            The new setting.
612         */
613        public void setReportSuccess(boolean report) {
614            connection.setReportSuccess(report); 
615        }
616    
617        /**
618         * Return the current startTLS property.
619         *
620         * @return The current startTLS property.
621         */
622        public boolean getStartTLS() {
623            return connection.getStartTLS(); 
624        }
625    
626        /**
627         * Set a new value for the startTLS property.
628         *
629         * @param start
630         *            The new setting.
631         */
632        public void setStartTLS(boolean start) {
633            connection.setStartTLS(start); 
634        }
635    
636        /**
637         * Retrieve the SASL realm used for DIGEST-MD5 authentication. This will
638         * either be explicitly set, or retrieved using the mail.smtp.sasl.realm
639         * session property.
640         *
641         * @return The current realm information (which can be null).
642         */
643        public String getSASLRealm() {
644            return connection.getSASLRealm(); 
645        }
646    
647        /**
648         * Explicitly set the SASL realm used for DIGEST-MD5 authenticaiton.
649         *
650         * @param name
651         *            The new realm name.
652         */
653        public void setSASLRealm(String name) {
654            connection.setSASLRealm(name); 
655        }
656    }