View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *  http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.geronimo.javamail.transport.smtp;
21  
22  import java.io.IOException;
23  import java.io.UnsupportedEncodingException;
24  import java.net.Socket; 
25  import java.util.ArrayList;
26  
27  import javax.mail.Address;
28  import javax.mail.AuthenticationFailedException;
29  import javax.mail.Message;
30  import javax.mail.MessagingException;
31  import javax.mail.Session;
32  import javax.mail.Transport;
33  import javax.mail.URLName;
34  import javax.mail.event.TransportEvent;
35  import javax.mail.internet.InternetAddress;
36  import javax.mail.internet.MimeMessage;
37  import javax.mail.internet.MimeMultipart;     
38  import javax.mail.internet.MimePart;     
39  
40  import org.apache.geronimo.javamail.util.ProtocolProperties; 
41  import org.apache.geronimo.javamail.transport.smtp.SMTPConnection.SendStatus; 
42  
43  /**
44   * Simple implementation of SMTP transport. Just does plain RFC821-ish delivery.
45   * <p/> Supported properties : <p/>
46   * <ul>
47   * <li> mail.host : to set the server to deliver to. Default = localhost</li>
48   * <li> mail.smtp.port : to set the port. Default = 25</li>
49   * <li> mail.smtp.locahost : name to use for HELO/EHLO - default getHostName()</li>
50   * </ul>
51   * <p/> There is no way to indicate failure for a given recipient (it's possible
52   * to have a recipient address rejected). The sun impl throws exceptions even if
53   * others successful), but maybe we do a different way... <p/> TODO : lots.
54   * ESMTP, user/pass, indicate failure, etc...
55   *
56   * @version $Rev: 673649 $ $Date: 2008-07-03 06:37:56 -0400 (Thu, 03 Jul 2008) $
57   */
58  public class SMTPTransport extends Transport {
59      /**
60       * property keys for protocol properties. The actual property name will be
61       * appended with "mail." + protocol + ".", where the protocol is either
62       * "smtp" or "smtps".
63       */
64      protected static final String MAIL_SMTP_DSN_NOTIFY = "dsn.notify";
65      protected static final String MAIL_SMTP_SENDPARTIAL = "sendpartial";
66      protected static final String MAIL_SMTP_EXTENSION = "mailextension";
67      protected static final String DEFAULT_MAIL_HOST = "localhost";
68  
69      protected static final int DEFAULT_MAIL_SMTP_PORT = 25;
70      protected static final int DEFAULT_MAIL_SMTPS_PORT = 465;
71  
72  
73      // do we use SSL for our initial connection?
74      protected boolean sslConnection = false;
75      
76      // our accessor for protocol properties and the holder of 
77      // protocol-specific information 
78      protected ProtocolProperties props; 
79      // our active connection object 
80      protected SMTPConnection connection;
81  
82      // the last response line received from the server.
83      protected SMTPReply lastServerResponse = null;
84  
85      /**
86       * Normal constructor for an SMTPTransport() object. This constructor is
87       * used to build a transport instance for the "smtp" protocol.
88       *
89       * @param session
90       *            The attached session.
91       * @param name
92       *            An optional URLName object containing target information.
93       */
94      public SMTPTransport(Session session, URLName name) {
95          this(session, name, "smtp", DEFAULT_MAIL_SMTP_PORT, false);
96      }
97      
98  
99      /**
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 }