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 }