001: /*
002: * The contents of this file are subject to the Sapient Public License
003: * Version 1.0 (the "License"); you may not use this file except in compliance
004: * with the License. You may obtain a copy of the License at
005: * http://carbon.sf.net/License.html.
006: *
007: * Software distributed under the License is distributed on an "AS IS" basis,
008: * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for
009: * the specific language governing rights and limitations under the License.
010: *
011: * The Original Code is The Carbon Component Framework.
012: *
013: * The Initial Developer of the Original Code is Sapient Corporation
014: *
015: * Copyright (C) 2003 Sapient Corporation. All Rights Reserved.
016: */
017:
018: package org.sape.carbon.services.email;
019:
020: import java.io.UnsupportedEncodingException;
021: import java.net.MalformedURLException;
022: import java.net.URL;
023: import java.util.ArrayList;
024: import java.util.Collection;
025: import java.util.HashMap;
026: import java.util.Iterator;
027: import java.util.Map;
028: import java.util.Properties;
029:
030: import javax.activation.DataHandler;
031: import javax.activation.DataSource;
032: import javax.activation.URLDataSource;
033: import javax.mail.BodyPart;
034: import javax.mail.Message;
035: import javax.mail.MessagingException;
036: import javax.mail.Multipart;
037: import javax.mail.Part;
038: import javax.mail.Session;
039: import javax.mail.Transport;
040: import javax.mail.internet.InternetAddress;
041: import javax.mail.internet.MimeBodyPart;
042: import javax.mail.internet.MimeMessage;
043: import javax.mail.internet.MimeMultipart;
044: import javax.mail.internet.MimeUtility;
045:
046: import org.sape.carbon.core.component.ComponentConfiguration;
047: import org.sape.carbon.core.component.lifecycle.Configurable;
048: import org.sape.carbon.core.component.lifecycle.Startable;
049: import org.sape.carbon.core.config.InvalidConfigurationException;
050: import org.sape.carbon.core.exception.InvalidParameterException;
051: import org.sape.carbon.services.email.util.MailAttachment;
052: import org.sape.carbon.services.email.util.MailContentTypeEnum;
053:
054: import org.apache.commons.logging.Log;
055: import org.apache.commons.logging.LogFactory;
056:
057: /**
058: * <p>This is a synchronous implementation for sending emails over JavaMail API.
059: * </p>
060: * @since carbon 1.0
061: * @stereotype implementation
062: * @author Nitin Gulati, June 2002
063: * @version $Revision: 1.15 $($Author: dvoet $ / $Date: 2003/05/05 21:21:29 $)
064: * <br>Copyright 2002 Sapient
065: */
066: public class SynchronousMailService implements MailService,
067: Configurable, Startable {
068:
069: /**
070: * Provides a handle to Apache-commons logger
071: */
072: private Log log = LogFactory.getLog(this .getClass());
073:
074: /**
075: * <p>Holds the SMTP host name or IP address.</p>
076: */
077: private String smtpHost;
078:
079: /**
080: *<p>A boolean property indicating whether or not the email service will
081: * create and keep open a connection to the SMTP host for its lifetime.
082: * If set to true, the connection will not be closed until the email
083: * service is shutdown. If set to false a new connection will be opened
084: * for each email sent.</p>
085: */
086: private boolean holdConnection;
087:
088: /**
089: * <p>This indicates the number of retry attempts to be made with SMTP
090: * server to send an email.When trying to send an email the service may
091: * receive an exception back from JavaMail. This is usually the result
092: * of failure to connect to the email server. In this case the service
093: * will execute the following steps n (retry attempts) times.
094: * <ul>
095: * <li>Close the connection to the email server. </li>
096: * <li>Sleep for some time (configurable, default is half a second)</li>
097: * <li>Reopen the connection to the email server. </li>
098: * <li>Try to send the email again. </li>
099: * </ul></p>
100: */
101: private int retryAttempts;
102:
103: /**
104: * <p>Holds the session object required to create the <code>MimeMessage
105: * </code>.</p>
106: */
107: private Session session;
108:
109: /**
110: * <p>Holds the reference to the <code>Transport</code> obejct which
111: * does the actual job of sending the message.</p>
112: */
113: private Transport transport;
114:
115: /**
116: * <p>Time in milliseconds waited between send attempts when failure
117: * occurs.</p>
118: */
119: private long sleepRetryTime;
120:
121: /** Content type key for headers. */
122: protected static final String CONTENT_TYPE = "Content-Type";
123:
124: /** An empty string. */
125: protected static final String BLANK_STRING = "";
126:
127: /**
128: * @see MailService#sendMail(MailDataObject mailDataObject)
129: *
130: * @param mailDataObject the mail data object to send
131: * @exception MailFailureException Throws when the mail was not sent
132: * because of various reasons listed below :
133: * <ul>
134: * <li>Invalid configuration E.g wrong ip of smtp server.</li>
135: * <li>The path or URL of any of the attachment does not exist.</li>
136: * <li>The charset passed for encoding the message is not supported.</li>
137: * <li>Unable to connect to the smtp server due to network issues.</li>
138: * </ul>
139: */
140: public void sendMail(MailDataObject mailDataObject)
141: throws MailFailureException {
142:
143: // Get all the parameters specified in mail data object.
144: Map to = mailDataObject.getToMap();
145: Map cc = mailDataObject.getCCMap();
146: Map bcc = mailDataObject.getBCCMap();
147: Map headers = mailDataObject.getHeaders();
148: String fromPersonal = mailDataObject.getFromName();
149: String fromEmail = mailDataObject.getFromEmail();
150: String subject = mailDataObject.getSubject();
151: String body = mailDataObject.getBody();
152: MailContentTypeEnum bodyType = mailDataObject.getBodyType();
153: String charset = mailDataObject.getCharset();
154: MailAttachment[] attachments = mailDataObject.getAttachments();
155:
156: // Check the mandatory fields before attempting to create the
157: // Mime message.
158: Collection errors = checkRequiredFields(to, cc, bcc, fromEmail,
159: bodyType);
160: if (!errors.isEmpty()) {
161: throw new InvalidParameterException(this .getClass(),
162: "Some of the required parameters are not specified : "
163: + processRequiredFieldErrors(errors));
164: }
165: // Create the Mime Message.
166: MimeMessage message = new MimeMessage(this .session);
167:
168: try {
169: // Set the from field.
170: message.setFrom(new InternetAddress(fromEmail, encodeText(
171: fromPersonal, charset, null)));
172:
173: // Set the TO addresses for the email.
174: if (to != null) {
175: setAddresses(message, to, Message.RecipientType.TO,
176: charset);
177: }
178:
179: // Set the CC addresses for the email.
180: if (cc != null) {
181: setAddresses(message, cc, Message.RecipientType.CC,
182: charset);
183: }
184:
185: // Set the BCC addresses for the email.
186: if (bcc != null) {
187: setAddresses(message, bcc, Message.RecipientType.BCC,
188: charset);
189: }
190:
191: message.setSubject(subject, charset);
192:
193: Iterator iter = null;
194: Map.Entry entry = null;
195:
196: // Set headers.
197: if (headers != null) {
198: iter = headers.entrySet().iterator();
199: while (iter.hasNext()) {
200: entry = (Map.Entry) iter.next();
201: message.addHeader((String) entry.getKey(),
202: (String) entry.getValue());
203: }
204: }
205:
206: // Set attachments.
207: if (attachments != null) {
208: processAttachments(message, body, bodyType, charset,
209: attachments);
210: } else {
211: // Fill the message
212: message.setText(body, charset);
213:
214: // Set the contentType of the message in the internet header.
215: setContentTypeHeader(message, bodyType, charset);
216: }
217:
218: } catch (UnsupportedEncodingException uee) {
219: throw new MailFailureException(
220: this .getClass(),
221: "The charset passed to encode the message is not supported.",
222: uee);
223: } catch (MalformedURLException mue) {
224: throw new MailFailureException(
225: this .getClass(),
226: "Failed to locate one of the attachment passed. "
227: + "Check the path or URL for each attachment.",
228: mue);
229: } catch (MessagingException me) {
230: throw new MailFailureException(this .getClass(),
231: "Failed to create the Mime Message", me);
232: }
233:
234: // Now send the message
235: try {
236: send(message);
237: } catch (MessagingException me) {
238: throw new MailFailureException(this .getClass(),
239: "Failed to send the mail", me);
240: }
241: }
242:
243: /**
244: * <p>Used internally to attach the attachments in the the <code>
245: * MimeMessage</code>
246: *
247: * @param message message to add the attachments to
248: * @param body The body content of the message
249: * @param bodyType type of body (plain/html/etc)
250: * @param charset character set of the message
251: * @param attachments array of attachments to add to the message
252: *
253: * @throws MalformedURLException indicates the path or URL of
254: * attachment does not exist.
255: * @throws UnsupportedEncodingException indicates the encoding given
256: * was not valid
257: * @throws MessagingException indicates a generic exception
258: * attempting to add the attachment
259: */
260: protected void processAttachments(MimeMessage message, String body,
261: MailContentTypeEnum bodyType, String charset,
262: MailAttachment[] attachments) throws MessagingException,
263: UnsupportedEncodingException, MalformedURLException {
264:
265: // Create the message part
266: MimeBodyPart messageBodyPart = new MimeBodyPart();
267:
268: // Fill the message
269: messageBodyPart.setText(body, charset);
270:
271: // Set the contentType of the message in the internet header.
272: setContentTypeHeader(messageBodyPart, bodyType, charset);
273:
274: // Create the MultiPart.
275: Multipart multipart = new MimeMultipart();
276:
277: multipart.addBodyPart(messageBodyPart);
278:
279: for (int k = 0; k < attachments.length; k++) {
280: multipart.addBodyPart(attach(attachments[k], charset));
281: }
282:
283: message.setContent(multipart);
284: }
285:
286: /**
287: * <p> This method is used internally to check the required parameters
288: * before sending an email. </p>
289: *
290: * @param to A Map containing the mapping of toEmail to toPersonal.
291: * @param cc A Map containing the mapping of ccEmail to ccPersonal.
292: * @param bcc A Map containing the mapping of bccEmail to bccPersonal.
293: * @param fromEmail sender's email id.
294: * @param bodyType the type of body content (plain/html/etc)
295: * @return Collection A collection containing all the errors.
296: */
297: protected Collection checkRequiredFields(Map to, Map cc, Map bcc,
298: String fromEmail, MailContentTypeEnum bodyType) {
299:
300: ArrayList errors = new ArrayList();
301:
302: // Check the presence of fromEmail
303: if (fromEmail == null) {
304: errors.add("You must specify the sender's email id.");
305: }
306:
307: // Check the presence of body type.
308: if (bodyType == null) {
309: errors.add("You must specify the body type.");
310: }
311:
312: // Check the presence of atleast one recipient.
313: HashMap allRecipients = new HashMap();
314:
315: // added validation for key (email address) to be null or
316: // blank string for to, cc and bcc Maps. This will prevent
317: // an invalid request to be sent to SMTP server
318: if (to != null) {
319: if (!(to.containsKey(null))
320: && (!(to.containsKey(BLANK_STRING)))) {
321:
322: allRecipients.putAll(to);
323: }
324: }
325:
326: if (cc != null) {
327: if (!(cc.containsKey(null))
328: && (!(cc.containsKey(BLANK_STRING)))) {
329:
330: allRecipients.putAll(cc);
331: }
332: }
333:
334: if (bcc != null) {
335: if (!(bcc.containsKey(null))
336: && (!(bcc.containsKey(BLANK_STRING)))) {
337:
338: allRecipients.putAll(bcc);
339: }
340: }
341:
342: if (allRecipients.isEmpty()) {
343: errors.add("You must specify atleast one recipient");
344: }
345:
346: return errors;
347: }
348:
349: /**
350: * <p> Used internally to process the validation errors. </p>
351: *
352: * @param errors collection of validation error to process
353: *
354: * @return The String representation of all the errors.
355: */
356: protected String processRequiredFieldErrors(Collection errors) {
357:
358: Iterator iterator = errors.iterator();
359: StringBuffer errorBuffer = new StringBuffer();
360:
361: while (iterator.hasNext()) {
362: errorBuffer.append(iterator.next()).append("\n");
363: }
364: return errorBuffer.toString();
365: }
366:
367: /**
368: * <p> This method sets the Content type header. </p>
369: *
370: * @param part The message or body part.
371: * @param bodyType The body type of the message or body part.
372: * @param charset The charset to be used for encoding.
373: * @exception MessagingException If this part is read-only.
374: */
375: protected void setContentTypeHeader(Part part,
376: MailContentTypeEnum bodyType, String charset)
377: throws MessagingException {
378:
379: if (charset != null) {
380: part.addHeader(SynchronousMailService.CONTENT_TYPE,
381: bodyType.getName() + "; charset=" + charset);
382: } else {
383: part.addHeader(SynchronousMailService.CONTENT_TYPE,
384: bodyType.getName());
385: }
386: }
387:
388: /**
389: * <p>Encode a RFC 822 "text" token into mail-safe form as per RFC 2047.
390: * </p>
391: *
392: * @param text The text to be encoded.
393: * @param charset The charset. If this parameter is null, the JVM's
394: * default charset is used.
395: * @param encodingType The encoding to be used. Currently supported
396: * values by Java Mail are "B" and "Q". If this parameter is
397: * null, then the "Q" encoding is used if most of characters
398: * to be encoded are in the ASCII charset, otherwise "B"
399: * encoding is used.
400: *
401: * @return Unicode string containing only US-ASCII characters
402: *
403: * @throws UnsupportedEncodingException Thrown when the text passed
404: * is non-ascii and the charset or encodingType is not supported.
405: */
406: protected String encodeText(String text, String charset,
407: String encodingType) throws UnsupportedEncodingException {
408:
409: String encodedText = null;
410:
411: /* In case of null the MimeUtility.encodeText would throw a
412: * NullPointerException. Because this method is used also to
413: * encode non-mandatory fields we will allow <code>null</code> values.
414: */
415: if (text != null) {
416: encodedText = MimeUtility.encodeText(text, charset,
417: encodingType);
418: }
419:
420: return encodedText;
421: }
422:
423: /** Constant for the file:// protocol. */
424: private static final String FILE = "file";
425:
426: /** Constant for the localhost address. */
427: private static final String LOCALHOST = "localhost";
428:
429: /**
430: * Attaches the <code>MailAttachment</code>.
431: *
432: * @param attachment A <code>MailAttachment</code>.
433: * @param charset the character of the attachment
434: * @return A Multipart.
435: *
436: * @throws MalformedURLException indicates the path or URL of
437: * attachment does not exist.
438: * @throws UnsupportedEncodingException indicates the encoding given
439: * was not valid
440: * @throws MessagingException indicates a generic exception
441: * attempting to add the attachment
442: */
443: protected BodyPart attach(MailAttachment attachment, String charset)
444: throws MalformedURLException, UnsupportedEncodingException,
445: MessagingException {
446:
447: // Get the url
448: URL url = attachment.getURL();
449:
450: String path = null;
451:
452: // If url is null try constructing a url with the path specified.
453: if (url == null) {
454: path = attachment.getPath();
455: url = new URL(FILE, LOCALHOST, path);
456: }
457:
458: // Now Create the MimeMultipart.
459: MimeBodyPart mimeBodyPart = new MimeBodyPart();
460:
461: // Create the data source for the URL
462: DataSource source = new URLDataSource(url);
463:
464: // Set the data handler
465: mimeBodyPart.setDataHandler(new DataHandler(source));
466:
467: mimeBodyPart.setFileName(encodeText(attachment.getName(),
468: charset, null));
469:
470: mimeBodyPart.setDescription(encodeText(attachment
471: .getDescription(), charset, null));
472:
473: return mimeBodyPart;
474: }
475:
476: /**
477: * <p> Used internally to set the addresses in <code>MimeMessage</code>
478: * from a Map structure. </p>
479: *
480: * @param message message to add the addresses to
481: * @param addresses map of addresses to add to the message
482: * @param recipientType type of message recipient
483: * @param charset character set of the address
484: *
485: * @throws MessagingException indicates a generic error trying to
486: * set the address
487: * @throws UnsupportedEncodingException indicates an unsupported
488: * encoding was given to the system.
489: */
490: protected void setAddresses(MimeMessage message, Map addresses,
491: Message.RecipientType recipientType, String charset)
492: throws MessagingException, UnsupportedEncodingException {
493:
494: Iterator iterator = null;
495: Map.Entry entry = null;
496: InternetAddress address = null;
497:
498: iterator = addresses.entrySet().iterator();
499: while (iterator.hasNext()) {
500: entry = (Map.Entry) iterator.next();
501: address = new InternetAddress(
502: (String) entry.getKey(),
503: encodeText((String) entry.getValue(), charset, null));
504: message.addRecipient(recipientType, address);
505: }
506: }
507:
508: /** Key for mail session property of protocol. */
509: private static final String MAIL_PROTOCOL_KEY = "mail.transport.protocol";
510:
511: /** Key for mail session property of mail host. */
512: private static final String MAIL_HOST = "mail.host";
513:
514: /** SMPT Value for mail session property of protocol. */
515: private static final String SMTP = "smtp";
516:
517: /**
518: * @see Configurable#configure(ComponentConfiguration)
519: */
520: public void configure(ComponentConfiguration config)
521: throws Exception {
522:
523: MailConfiguration mailConfig = (MailConfiguration) config;
524:
525: // Get all the configuration parameters.
526: this .smtpHost = mailConfig.getSmtpHost();
527: this .holdConnection = mailConfig.getHoldConnection();
528: this .retryAttempts = mailConfig.getRetryAttempts();
529:
530: // Check for -ve retry attempts.
531: if (this .retryAttempts < 0) {
532: throw new InvalidConfigurationException(this .getClass(),
533: mailConfig.getConfigurationName(), "RetryAttempts",
534: "Retry Attempts should be >= 0");
535: }
536:
537: // If retry Attempts is 0 then we don't need the sleep time.
538: if (this .retryAttempts > 0) {
539: this .sleepRetryTime = mailConfig.getSleepTimeInMilliSecs();
540: }
541:
542: // Check for -ve sleep time
543: if (sleepRetryTime < 0) {
544: throw new InvalidConfigurationException(this .getClass(),
545: mailConfig.getConfigurationName(),
546: "SleepTimeInMilliSecs", "sleep time should be >= 0");
547: }
548:
549: Properties props = new Properties();
550: // Get all the properties required to create session.
551: props.put(MAIL_PROTOCOL_KEY, SMTP);
552: props.put(MAIL_HOST, this .smtpHost);
553:
554: /* Create the session object required to create messages.
555: * Note, do not use Session.getDefaultInstance to create
556: * Session here because there might be other email components
557: * with different configurations in the system OR you might
558: * want to change the configuration of the component at run time.
559: * In both the cases the Session.getDefaultInstance will return
560: * the session object with previous configuration.
561: */
562: this .session = Session.getInstance(props, null);
563: this .session.setDebug(mailConfig.getJavaMailDebugMode());
564:
565: // Initilize transport object, which does the actual job of
566: // sending messages.
567: this .transport = this .session.getTransport();
568: }
569:
570: /**
571: * <p>Sends a single email message.</p>
572: *
573: * @param message The message to be sent.
574: * @throws MessagingException indicates an error sending the message
575: */
576: protected void send(MimeMessage message) throws MessagingException {
577:
578: int attempts = 0;
579: boolean messageSent = false;
580: MessagingException exception = null;
581:
582: while (attempts < (this .retryAttempts + 1) && !messageSent) {
583: try {
584: sendInternal(message);
585: messageSent = true;
586: } catch (MessagingException me) {
587: // Close the connection, if Open
588: if (this .transport.isConnected()) {
589: this .transport.close();
590: }
591: // Sleep for some time.
592: try {
593: Thread.sleep(this .sleepRetryTime);
594: } catch (InterruptedException ie) {
595: /* Do Nothing */
596: }
597:
598: // Retain the reference of the exception.
599: exception = me;
600: attempts++;
601: }
602: }
603:
604: if (!messageSent) {
605: if (log.isWarnEnabled()) {
606: log.warn("unable to send the message :" + message);
607: }
608:
609: throw exception;
610: } else {
611: if (log.isInfoEnabled()) {
612: log.info("sent the message:" + message);
613: }
614: }
615: }
616:
617: /**
618: * <p>Does the actual job of sending a message. The call to this method
619: * is synchronized on this.transport because different threads can access
620: * the same component to send emails. If the call is not synchronized then
621: * there might be the case when one thread opens a connection, the
622: * second thread sends an email and closes the connection. Now first
623: * threads wakes up and try to send an email but would get an
624: * exception as the connection is closed. </p>
625: *
626: * @param message The message to be sent.
627: * @throws MessagingException indicates an error sending the message
628: */
629: protected void sendInternal(MimeMessage message)
630: throws MessagingException {
631:
632: synchronized (this .transport) {
633: // Double check the connection
634: if (!this .transport.isConnected()) {
635: this .transport.connect();
636: }
637: // Send the message
638: this .transport.sendMessage(message, message
639: .getAllRecipients());
640: // Check for hold connection
641: if (!this .holdConnection) {
642: this .transport.close();
643: }
644: }
645: }
646:
647: /**
648: * @see Startable#stop()
649: */
650: public void stop() throws Exception {
651:
652: // Close the transport if already opened.
653: if (this .transport != null && this .transport.isConnected()) {
654: transport.close();
655: }
656: }
657:
658: /**
659: * @see Startable#start()
660: */
661: public void start() {
662: // No work to do at this point of time.
663: }
664: }
|