Java tutorial
/********************************************************************************** * $URL$ * $Id$ *********************************************************************************** * * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 Sakai Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * **********************************************************************************/ package org.sakaiproject.email.impl; import java.io.UnsupportedEncodingException; import javax.mail.internet.MimeUtility; import java.io.IOException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.StringTokenizer; import java.util.Map.Entry; import javax.activation.DataHandler; import javax.activation.DataSource; import javax.mail.Address; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.Multipart; import javax.mail.SendFailedException; import javax.mail.Session; import javax.mail.Transport; import javax.mail.internet.AddressException; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMultipart; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.component.api.ServerConfigurationService; import org.sakaiproject.email.api.AddressValidationException; import org.sakaiproject.email.api.Attachment; import org.sakaiproject.email.api.CharacterSet; import org.sakaiproject.email.api.ContentType; import org.sakaiproject.email.api.EmailAddress; import org.sakaiproject.email.api.EmailAddress.RecipientType; import org.sakaiproject.email.api.EmailHeaders; import org.sakaiproject.email.api.EmailMessage; import org.sakaiproject.email.api.EmailService; import org.sakaiproject.email.api.NoRecipientsException; import org.sakaiproject.user.api.User; /** * <p> * BasicEmailService implements the EmailService. * </p> */ public class BasicEmailService implements EmailService { /** Our logger. */ private static Log M_log = LogFactory.getLog(BasicEmailService.class); protected static final String PROTOCOL_SMTP = "smtp"; protected static final String PROTOCOL_SMTPS = "smtps"; protected static final String POSTMASTER = "postmaster"; /** As defined in the com.sun.mail.smtp part of javamail. */ /** The SMTP server to connect to. */ public static final String MAIL_HOST_T = "mail.%1$s.host"; /** * The SMTP server port to connect to, if the connect() method doesn't explicitly specify one. * Defaults to 25. */ public static final String MAIL_PORT_T = "mail.%1$s.port"; /** * Email address to use for SMTP MAIL command. This sets the envelope return address. Defaults * to msg.getFrom() or InternetAddress.getLocalAddress(). NOTE: mail.smtp.user was previously * used for this. */ public static final String MAIL_FROM_T = "mail.%1$s.from"; /** * If set to true, and a message has some valid and some invalid addresses, send the message * anyway, reporting the partial failure with a SendFailedException. If set to false (the * default), the message is not sent to any of the recipients if there is an invalid recipient * address. */ public static final String MAIL_SENDPARTIAL_T = "mail.%1$s.sendpartial"; /** Socket connection timeout value in milliseconds. Default is infinite timeout. */ public static final String MAIL_CONNECTIONTIMEOUT_T = "mail.%1$s.connectiontimeout"; /** Socket I/O timeout value in milliseconds. Default is infinite timeout. */ public static final String MAIL_TIMEOUT_T = "mail.%1$s.timeout"; /** Whether to authenticate when connecting to the mail server */ public static final String MAIL_AUTH_T = "mail.%1$s.auth"; /** Whether to enable the starting TLS */ public static final String MAIL_TLS_ENABLE_T = "mail.%1$s.starttls.enable"; /** What socket factory to use when connecting over SSL */ public static final String MAIL_SOCKET_FACTORY_CLASS_T = "mail.%1$s.socketFactory.class"; /** Whether to fallback to a different protocol if first attempt fails. */ public static final String MAIL_SOCKET_FACTORY_FALLBACK_T = "mail.%1$s.socketFactory.fallback"; /** Hostname used in outgoing SMTP HELO commands. */ public static final String MAIL_LOCALHOST_T = "mail.%1$s.localhost"; /** Class name of SSL socket factory */ public static final String SSL_FACTORY = "javax.net.ssl.SSLSocketFactory"; /** Whether to turn on mail debugging */ public static final String MAIL_DEBUG = "mail.debug"; public static final String MAIL_SENDFROMSAKAI = "mail.sendfromsakai"; public static final String MAIL_SENDFROMSAKAI_EXCEPTIONS = "mail.sendfromsakai.exceptions"; public static final String MAIL_SENDFROMSAKAI_FROMTEXT = "mail.sendfromsakai.fromtext"; protected static final String CONTENT_TYPE = ContentType.TEXT_PLAIN; protected ServerConfigurationService serverConfigurationService; public void setServerConfigurationService(ServerConfigurationService serverConfigurationService) { this.serverConfigurationService = serverConfigurationService; } /** The protocol to use when connecting to the mail server */ private String protocol; /********************************************************************************************************************************************************************************************************************************************************** * Configuration Note: keep these in sync with the TestEmailService, to make switching between them easier -ggolden *********************************************************************************************************************************************************************************************************************************************************/ /** Configuration: smtp server to use. */ protected String m_smtp = null; /** * Configuration: smtp server to use. * * @param value * The smtp server string. */ public void setSmtp(String value) { m_smtp = value; } /** Configuration: smtp server port to use. */ protected String m_smtpPort = null; /** * Configuration: smtp server port to use. * * @param value * The smtp server port string. */ public void setSmtpPort(String value) { m_smtpPort = value; } /** Configuration: smtp user for use with authenticated SMTP. */ protected String m_smtpUser = null; /** * Configuration: smtp user for use with authenticated SMTP. * * @param value * The smtp user string. */ public void setSmtpUser(String value) { m_smtpUser = value; } /** Configuration: smtp password for use with authenticated SMTP. */ protected String m_smtpPassword = null; /** * Configuration: smtp password for use with authenticated SMTP. * * @param value * The smtp password string. */ public void setSmtpPassword(String value) { m_smtpPassword = value; } /** Configuration: send over SSL (or not) */ protected boolean m_smtpUseSSL; /** * Configuration: send over SSL (or not) * * @param useSSL * The setting */ public void setSmtpUseSSL(boolean useSSL) { m_smtpUseSSL = useSSL; } /** Configuration: send using TLS (or not). */ protected boolean m_smtpUseTLS = false; /** * Configuration: send using TLS (or not) * * @param value * The setting */ public void setSmtpUseTLS(boolean value) { m_smtpUseTLS = value; } /** Configuration: set the mail.debug property so we can get proper output from javamail (or not). */ protected boolean m_smtpDebug = false; /** * Configuration: set the mail.debug property so we can get proper output from javamail (or not). * * @param value * The setting */ public void setSmtpDebug(boolean value) { m_smtpDebug = value; } /** Configuration: optional smtp mail envelope return address. */ protected String m_smtpFrom = null; /** * Configuration: smtp mail envelope return address. * * @param value * The smtp mail from address string. */ public void setSmtpFrom(String value) { m_smtpFrom = value; } /** Configuration: set to go into test mode, where mail is not really sent out. */ protected boolean m_testMode = false; /** * Configuration: set test mode. * * @param value * The test mode value */ public void setTestMode(boolean value) { m_testMode = value; } /** * Configuration: turns off transport sending. This can be turned off to allow pr * happen normally but only stop calling Transport.send. Allows the code to run t * checks and validations that testMode = true does not. */ protected boolean allowTransport = true; public void setAllowTransport(boolean allowTransport) { this.allowTransport = allowTransport; } /** The max # recipients to include in each message. */ protected int m_maxRecipients = 100; /** * Set max # recipients to include in each message. * * @param setting * The max # recipients to include in each message. (as an integer string). */ public void setMaxRecipients(String setting) { m_maxRecipients = Integer.parseInt(setting); // validate - if invalid, restore to the default if (m_maxRecipients < 1) m_maxRecipients = 100; } /** Configuration: use a connection to the SMTP for only one message (or not). */ protected boolean m_oneMessagePerConnection = false; /** * Configuration: set use a connection to the SMTP for only one message (or not) * * @param value * The setting */ public void setOneMessagePerConnection(boolean value) { m_oneMessagePerConnection = value; } /** Hostname to use for SMTP HELO commands */ protected String m_smtpLocalhost = null; /** * Hostname to use for SMTP HELO commands. * RFC1123 section 5.2.5 and RFC2821 section 4.1.1.1 * * @param value * The hostname (eg foo.example.com) */ public void setSmtpLocalhost(String value) { m_smtpLocalhost = value; } /** Configuration: send partial email or fail on any errors */ protected boolean m_sendPartial = true; public void setSendPartial(boolean sendPartial) { m_sendPartial = sendPartial; } /** Configuration: Socket connection timeout value in milliseconds. Default is infinite timeout. */ protected String m_smtpConnectionTimeout = null; /** Configuration: Socket I/O timeout value in milliseconds. Default is infinite timeout. */ protected String m_smtpTimeout = null; /********************************************************************************************************************************************************************************************************************************************************** * Init and Destroy *********************************************************************************************************************************************************************************************************************************************************/ /** * Final initialization, once all dependencies are set. */ public void init() { // set the protocol to be used if (m_smtpUseSSL) { protocol = PROTOCOL_SMTPS; } else { protocol = PROTOCOL_SMTP; } // if no m_mailfrom set, set to the postmaster if (m_smtpFrom == null) { m_smtpFrom = POSTMASTER + "@" + serverConfigurationService.getServerName(); } // initialize timeout values m_smtpConnectionTimeout = serverConfigurationService.getString(propName(MAIL_CONNECTIONTIMEOUT_T), null); m_smtpTimeout = serverConfigurationService.getString(propName(MAIL_TIMEOUT_T), null); // check for smtp protocol labeled values for backwards compatibility if (PROTOCOL_SMTPS.equals(protocol)) { if (m_smtpConnectionTimeout == null) m_smtpConnectionTimeout = serverConfigurationService .getString(propName(MAIL_CONNECTIONTIMEOUT_T, PROTOCOL_SMTP), null); if (m_smtpTimeout == null) m_smtpTimeout = serverConfigurationService.getString(propName(MAIL_TIMEOUT_T, PROTOCOL_SMTP), null); } // promote these to the system properties, to keep others (James) from messing with them if (m_smtp != null) System.setProperty(propName(MAIL_HOST_T), m_smtp); if (m_smtpPort != null) System.setProperty(propName(MAIL_PORT_T), m_smtpPort); System.setProperty(propName(MAIL_FROM_T), m_smtpFrom); if (m_smtpConnectionTimeout != null) System.setProperty(propName(MAIL_CONNECTIONTIMEOUT_T), m_smtpConnectionTimeout); if (m_smtpTimeout != null) System.setProperty(propName(MAIL_TIMEOUT_T), m_smtpTimeout); M_log.info("init(): smtp: " + m_smtp + ((m_smtpPort != null) ? (":" + m_smtpPort) : "") + " bounces to: " + m_smtpFrom + " maxRecipients: " + m_maxRecipients + " testMode: " + m_testMode + ((m_smtpConnectionTimeout != null) ? (" smtpConnectionTimeout: " + m_smtpConnectionTimeout) : "") + ((m_smtpTimeout != null) ? (" smtpTimeout: " + m_smtpTimeout) : "")); } /** * Final cleanup. */ public void destroy() { M_log.info("destroy()"); } /********************************************************************************************************************************************************************************************************************************************************** * Work interface methods: org.sakai.service.email.EmailService *********************************************************************************************************************************************************************************************************************************************************/ /** * {@inheritDoc} */ public void sendMail(InternetAddress from, InternetAddress[] to, String subject, String content, InternetAddress[] headerTo, InternetAddress[] replyTo, List<String> additionalHeaders) { HashMap<RecipientType, InternetAddress[]> recipients = null; if (headerTo != null) { recipients = new HashMap<RecipientType, InternetAddress[]>(); recipients.put(RecipientType.TO, headerTo); } sendMail(from, to, subject, content, recipients, replyTo, additionalHeaders, null); } /** * {@inheritDoc} */ public void sendMail(InternetAddress from, InternetAddress[] to, String subject, String content, Map<RecipientType, InternetAddress[]> headerTo, InternetAddress[] replyTo, List<String> additionalHeaders, List<Attachment> attachments) { try { sendMailMessagingException(from, to, subject, content, headerTo, replyTo, additionalHeaders, attachments); } catch (MessagingException e) { M_log.error("Email.sendMail: exception: " + e.getMessage(), e); } } public void sendMailMessagingException(InternetAddress from, InternetAddress[] to, String subject, String content, Map<RecipientType, InternetAddress[]> headerTo, InternetAddress[] replyTo, List<String> additionalHeaders, List<Attachment> attachments) throws MessagingException { // some timing for debug long start = 0; if (M_log.isDebugEnabled()) start = System.currentTimeMillis(); // if in test mode, use the test method if (m_testMode) { testSendMail(from, to, subject, content, headerTo, replyTo, additionalHeaders); return; } if (m_smtp == null) { M_log.error( "Unable to send mail as no smtp server is defined. Please set smtp@org.sakaiproject.email.api.EmailService value in sakai.properties"); return; } if (from == null) { M_log.warn("sendMail: null from"); return; } if (to == null) { M_log.warn("sendMail: null to"); return; } if (content == null) { M_log.warn("sendMail: null content"); return; } Properties props = createMailSessionProperties(); Session session = Session.getInstance(props); // see if we have a message-id in the additional headers String mid = null; if (additionalHeaders != null) { for (String header : additionalHeaders) { if (header.toLowerCase().startsWith(EmailHeaders.MESSAGE_ID.toLowerCase() + ": ")) { // length of 'message-id: ' == 12 mid = header.substring(12); break; } } } // use the special extension that can set the id MimeMessage msg = new MyMessage(session, mid); // the FULL content-type header, for example: // Content-Type: text/plain; charset=windows-1252; format=flowed String contentTypeHeader = null; // If we need to force the container to use a certain multipart subtype // e.g. 'alternative' // then sneak it through in the additionalHeaders String multipartSubtype = null; // set the additional headers on the message // but treat Content-Type specially as we need to check the charset // and we already dealt with the message id if (additionalHeaders != null) { for (String header : additionalHeaders) { if (header.toLowerCase().startsWith(EmailHeaders.CONTENT_TYPE.toLowerCase() + ": ")) contentTypeHeader = header; else if (header.toLowerCase().startsWith(EmailHeaders.MULTIPART_SUBTYPE.toLowerCase() + ": ")) multipartSubtype = header.substring(header.indexOf(":") + 1).trim(); else if (!header.toLowerCase().startsWith(EmailHeaders.MESSAGE_ID.toLowerCase() + ": ")) msg.addHeaderLine(header); } } // date if (msg.getHeader(EmailHeaders.DATE) == null) msg.setSentDate(new Date(System.currentTimeMillis())); // set the message sender msg.setFrom(from); // set the message recipients (headers) setRecipients(headerTo, msg); // set the reply to if ((replyTo != null) && (msg.getHeader(EmailHeaders.REPLY_TO) == null)) msg.setReplyTo(replyTo); // update to be Postmaster if necessary checkFrom(msg); // figure out what charset encoding to use // // first try to use the charset from the forwarded // Content-Type header (if there is one). // // if that charset doesn't work, try a couple others. // the character set, for example, windows-1252 or UTF-8 String charset = extractCharset(contentTypeHeader); if (charset != null && canUseCharset(content, charset) && canUseCharset(subject, charset)) { // use the charset from the Content-Type header } else if (canUseCharset(content, CharacterSet.ISO_8859_1) && canUseCharset(subject, CharacterSet.ISO_8859_1)) { if (contentTypeHeader != null && charset != null) contentTypeHeader = contentTypeHeader.replaceAll(charset, CharacterSet.ISO_8859_1); else if (contentTypeHeader != null) contentTypeHeader += "; charset=" + CharacterSet.ISO_8859_1; charset = CharacterSet.ISO_8859_1; } else if (canUseCharset(content, CharacterSet.WINDOWS_1252) && canUseCharset(subject, CharacterSet.WINDOWS_1252)) { if (contentTypeHeader != null && charset != null) contentTypeHeader = contentTypeHeader.replaceAll(charset, CharacterSet.WINDOWS_1252); else if (contentTypeHeader != null) contentTypeHeader += "; charset=" + CharacterSet.WINDOWS_1252; charset = CharacterSet.WINDOWS_1252; } else { // catch-all - UTF-8 should be able to handle anything if (contentTypeHeader != null && charset != null) contentTypeHeader = contentTypeHeader.replaceAll(charset, CharacterSet.UTF_8); else if (contentTypeHeader != null) contentTypeHeader += "; charset=" + CharacterSet.UTF_8; else contentTypeHeader = EmailHeaders.CONTENT_TYPE + ": " + ContentType.TEXT_PLAIN + "; charset=" + CharacterSet.UTF_8; charset = CharacterSet.UTF_8; } if ((subject != null) && (msg.getHeader(EmailHeaders.SUBJECT) == null)) msg.setSubject(subject, charset); // extract just the content type value from the header String contentType = null; if (contentTypeHeader != null) { int colonPos = contentTypeHeader.indexOf(":"); contentType = contentTypeHeader.substring(colonPos + 1).trim(); } setContent(content, attachments, msg, contentType, charset, multipartSubtype); // if we have a full Content-Type header, set it NOW // (after setting the body of the message so that format=flowed is preserved) // if there attachments, the messsage type will default to multipart/mixed and should // stay that way. if ((attachments == null || attachments.size() == 0) && contentTypeHeader != null) { msg.addHeaderLine(contentTypeHeader); msg.addHeaderLine(EmailHeaders.CONTENT_TRANSFER_ENCODING + ": quoted-printable"); } if (M_log.isDebugEnabled()) { M_log.debug("HeaderLines received were: "); Enumeration<String> allHeaders = msg.getAllHeaderLines(); while (allHeaders.hasMoreElements()) { M_log.debug((String) allHeaders.nextElement()); } } sendMessageAndLog(from, to, subject, headerTo, start, msg, session); } /** * fix up From and ReplyTo if we need it to be from Postmaster */ private void checkFrom(MimeMessage msg) { String sendFromSakai = serverConfigurationService.getString(MAIL_SENDFROMSAKAI, "true"); String sendExceptions = serverConfigurationService.getString(MAIL_SENDFROMSAKAI_EXCEPTIONS, null); InternetAddress from = null; InternetAddress[] replyTo = null; try { Address[] fromA = msg.getFrom(); if (fromA == null || fromA.length == 0) { M_log.info("message from missing"); return; } else if (fromA.length > 1) { M_log.info("message from more than 1"); return; } else if (fromA instanceof InternetAddress[]) { from = (InternetAddress) fromA[0]; } else { M_log.info("message from not InternetAddress"); return; } Address[] replyToA = msg.getReplyTo(); if (replyToA == null) replyTo = null; else if (replyToA instanceof InternetAddress[]) replyTo = (InternetAddress[]) replyToA; else { M_log.info("message replyto isn't internet address"); return; } // should we replace from address with a Sakai address? if (sendFromSakai != null && !sendFromSakai.equalsIgnoreCase("false")) { // exceptions -- addresses to leave alone. Our own addresses are always exceptions. // you can also configure a regexp of exceptions. if (!from.getAddress().toLowerCase() .endsWith("@" + serverConfigurationService.getServerName().toLowerCase()) && (sendExceptions == null || sendExceptions.equals("") || !from.getAddress().toLowerCase().matches(sendExceptions))) { // not an exception. do the replacement. // First, decide on the replacement address. The config variable // may be the replacement address. If not, use postmaster if (sendFromSakai.indexOf("@") < 0) sendFromSakai = POSTMASTER + "@" + serverConfigurationService.getServerName(); // put the original from into reply-to, unless a replyto exists if (replyTo == null || replyTo.length == 0 || replyTo[0].getAddress().equals("")) { replyTo = new InternetAddress[1]; replyTo[0] = from; msg.setReplyTo(replyTo); } // for some reason setReplyTo doesn't work, though setFrom does. Have to create the // actual header line if (msg.getHeader(EmailHeaders.REPLY_TO) == null) msg.addHeader(EmailHeaders.REPLY_TO, from.getAddress()); // and use the new from address // biggest issue is the "personal address", i.e. the comment text String origFromText = from.getPersonal(); String origFromAddress = from.getAddress(); String fromTextPattern = serverConfigurationService.getString(MAIL_SENDFROMSAKAI_FROMTEXT, "{}"); String fromText = null; if (origFromText != null && !origFromText.equals("")) fromText = fromTextPattern.replace("{}", origFromText + " (" + origFromAddress + ")"); else fromText = fromTextPattern.replace("{}", origFromAddress); from = new InternetAddress(sendFromSakai); try { from.setPersonal(fromText); } catch (Exception e) { } msg.setFrom(from); } } } catch (javax.mail.internet.AddressException e) { M_log.info("checkfrom address exception " + e); } catch (javax.mail.MessagingException e) { M_log.info("checkfrom messaging exception " + e); } } /** * {@inheritDoc} */ public void send(String fromStr, String toStr, String subject, String content, String headerToStr, String replyToStr, List<String> additionalHeaders) { // if in test mode, use the test method if (m_testMode) { testSend(fromStr, toStr, subject, content, headerToStr, replyToStr, additionalHeaders); return; } if (fromStr == null) { M_log.warn("send: null fromStr"); return; } if (toStr == null) { M_log.warn("send: null toStr"); return; } if (content == null) { M_log.warn("send: null content"); return; } try { InternetAddress from = new InternetAddress(fromStr); StringTokenizer tokens = new StringTokenizer(toStr, ", "); InternetAddress[] to = new InternetAddress[tokens.countTokens()]; int i = 0; while (tokens.hasMoreTokens()) { String next = (String) tokens.nextToken(); to[i] = new InternetAddress(next); i++; } // cycle through and collect all of the Internet addresses from the list. InternetAddress[] headerTo = null; if (headerToStr != null) { headerTo = new InternetAddress[1]; headerTo[0] = new InternetAddress(headerToStr); } InternetAddress[] replyTo = null; if (replyToStr != null) { replyTo = new InternetAddress[1]; replyTo[0] = new InternetAddress(replyToStr); } sendMail(from, to, subject, content, headerTo, replyTo, additionalHeaders); } catch (AddressException e) { M_log.warn("send: " + e); } } /** * {@inheritDoc} */ public void sendToUsers(Collection<User> users, Collection<String> headers, String message) { if (headers == null) { M_log.warn("sendToUsers: null headers"); return; } if (m_testMode) { M_log.info("sendToUsers: users: " + usersToStr(users) + " headers: " + listToStr(headers) + " message:\n" + message); return; } if (m_smtp == null) { M_log.warn("sendToUsers: smtp not set"); return; } if (users == null) { M_log.warn("sendToUsers: null users"); return; } if (message == null) { M_log.warn("sendToUsers: null message"); return; } // form the list of to: addresses from the users users collection ArrayList<InternetAddress> addresses = new ArrayList<InternetAddress>(); for (User user : users) { String email = user.getEmail(); if ((email != null) && (email.length() > 0)) { try { addresses.add(new InternetAddress(email)); } catch (AddressException e) { if (M_log.isDebugEnabled()) M_log.debug("sendToUsers: " + e); } } } // if we have none if (addresses.isEmpty()) return; // how many separate messages do we need to send to keep each one at or under m_maxRecipients? int numMessageSets = ((addresses.size() - 1) / m_maxRecipients) + 1; // make an array for each and store them all in the collection ArrayList<Address[]> messageSets = new ArrayList<Address[]>(); int posInAddresses = 0; for (int i = 0; i < numMessageSets; i++) { // all but the last one are max size int thisSize = m_maxRecipients; if (i == numMessageSets - 1) { thisSize = addresses.size() - ((numMessageSets - 1) * m_maxRecipients); } // size an array Address[] toAddresses = new Address[thisSize]; messageSets.add(toAddresses); // fill the array int posInToAddresses = 0; while (posInToAddresses < thisSize) { toAddresses[posInToAddresses] = (Address) addresses.get(posInAddresses); posInToAddresses++; posInAddresses++; } } // get a session for our smtp setup, include host, port, reverse-path, and set partial delivery Properties props = createMailSessionProperties(); Session session = Session.getInstance(props); // form our Message MimeMessage msg = new MyMessage(session, headers, message); // fix From and ReplyTo if necessary checkFrom(msg); // transport the message long time1 = 0; long time2 = 0; long time3 = 0; long time4 = 0; long time5 = 0; long time6 = 0; long timeExtraConnect = 0; long timeExtraClose = 0; long timeTmp = 0; int numConnects = 1; try { if (M_log.isDebugEnabled()) time1 = System.currentTimeMillis(); Transport transport = session.getTransport(protocol); if (M_log.isDebugEnabled()) time2 = System.currentTimeMillis(); msg.saveChanges(); if (M_log.isDebugEnabled()) time3 = System.currentTimeMillis(); if (m_smtpUser != null && m_smtpPassword != null) transport.connect(m_smtp, m_smtpUser, m_smtpPassword); else transport.connect(); if (M_log.isDebugEnabled()) time4 = System.currentTimeMillis(); // loop the send for each message set for (Iterator<Address[]> i = messageSets.iterator(); i.hasNext();) { Address[] toAddresses = i.next(); try { transport.sendMessage(msg, toAddresses); // if we need to use the connection for just one send, and we have more, close and re-open if ((m_oneMessagePerConnection) && (i.hasNext())) { if (M_log.isDebugEnabled()) timeTmp = System.currentTimeMillis(); transport.close(); if (M_log.isDebugEnabled()) timeExtraClose += (System.currentTimeMillis() - timeTmp); if (M_log.isDebugEnabled()) timeTmp = System.currentTimeMillis(); transport.connect(); if (M_log.isDebugEnabled()) { timeExtraConnect += (System.currentTimeMillis() - timeTmp); numConnects++; } } } catch (SendFailedException e) { if (M_log.isDebugEnabled()) M_log.debug("sendToUsers: " + e); } catch (MessagingException e) { M_log.warn("sendToUsers: " + e); } } if (M_log.isDebugEnabled()) time5 = System.currentTimeMillis(); transport.close(); if (M_log.isDebugEnabled()) time6 = System.currentTimeMillis(); } catch (MessagingException e) { M_log.warn("sendToUsers:" + e); } // log if (M_log.isInfoEnabled()) { StringBuilder buf = new StringBuilder(); buf.append("sendToUsers: headers["); for (String header : headers) { buf.append(" "); buf.append(cleanUp(header)); } buf.append("]"); for (Address[] toAddresses : messageSets) { buf.append(" to[ "); for (int a = 0; a < toAddresses.length; a++) { buf.append(" "); buf.append(toAddresses[a]); } buf.append("]"); } if (M_log.isDebugEnabled()) { buf.append(" times[ "); buf.append(" getransport:" + (time2 - time1) + " savechanges:" + (time3 - time2) + " connect(#" + numConnects + "):" + ((time4 - time3) + timeExtraConnect) + " send:" + (((time5 - time4) - timeExtraConnect) - timeExtraClose) + " close:" + ((time6 - time5) + timeExtraClose) + " total: " + (time6 - time1) + " ]"); } M_log.info(buf.toString()); } } private Properties createMailSessionProperties() { Properties props = new Properties(); props.put(propName(MAIL_HOST_T), m_smtp); // Set localhost name if (m_smtpLocalhost != null) props.put(propName(MAIL_LOCALHOST_T), m_smtpLocalhost); if (m_smtpPort != null) props.put(propName(MAIL_PORT_T), m_smtpPort); props.put(propName(MAIL_FROM_T), m_smtpFrom); props.put(propName(MAIL_SENDPARTIAL_T), Boolean.valueOf(m_sendPartial)); if (m_smtpConnectionTimeout != null) props.put(propName(MAIL_CONNECTIONTIMEOUT_T), m_smtpConnectionTimeout); if (m_smtpTimeout != null) props.put(propName(MAIL_TIMEOUT_T), m_smtpTimeout); // smtpUser and smtpPassword are set, so assume mail.smtp.auth if (m_smtpUser != null && m_smtpPassword != null) props.put(propName(MAIL_AUTH_T), Boolean.TRUE.toString()); if (m_smtpUseTLS) props.put(propName(MAIL_TLS_ENABLE_T), Boolean.TRUE.toString()); if (m_smtpUseSSL) { props.put(propName(MAIL_SOCKET_FACTORY_CLASS_T), SSL_FACTORY); props.put(propName(MAIL_SOCKET_FACTORY_FALLBACK_T), Boolean.FALSE.toString()); } if (m_smtpDebug) props.put(MAIL_DEBUG, Boolean.TRUE.toString()); return props; } /** * {@inheritDoc} * * @see org.sakaiproject.email.api.EmailService#send(EmailMessage) * For temporary backward compatibility */ public List<EmailAddress> send(EmailMessage msg) throws AddressValidationException, NoRecipientsException { List<EmailAddress> addresses = new ArrayList<EmailAddress>(); try { addresses = send(msg, true); } catch (MessagingException e) { M_log.error("Email.sendMail: exception: " + e.getMessage(), e); } return addresses; } /** * {@inheritDoc} * * @see org.sakaiproject.email.api.EmailService#send(EmailMessage) */ public List<EmailAddress> send(EmailMessage msg, boolean messagingException) throws AddressValidationException, NoRecipientsException, MessagingException { ArrayList<EmailAddress> invalids = new ArrayList<EmailAddress>(); InternetAddress from = null; // convert and validate the 'from' address try { from = new InternetAddress(msg.getFrom().getAddress(), true); from.setPersonal(msg.getFrom().getPersonal()); } catch (AddressException e) { throw new AddressValidationException("Invalid 'FROM' address: " + msg.getFrom().getAddress(), msg.getFrom()); } catch (UnsupportedEncodingException e) { throw new AddressValidationException("Invalid 'FROM' address: " + msg.getFrom().getAddress(), msg.getFrom()); } // convert and validate reply to addresses InternetAddress[] replyTo = emails2Internets(msg.getReplyTo(), invalids); if (!invalids.isEmpty()) { throw new AddressValidationException("Invalid 'REPLY TO' address", invalids); } /* * LOOK - IF THERE ARE ANY INVALID RECIPIENT, AN EXCEPTION IS THROWN AND THE METHOD EXITS */ // convert and validate the 'to' addresses InternetAddress[] to = emails2Internets(msg.getRecipients(RecipientType.TO), invalids); // convert and validate 'cc' addresses InternetAddress[] cc = emails2Internets(msg.getRecipients(RecipientType.CC), invalids); // convert and validate 'bcc' addresses InternetAddress[] bcc = emails2Internets(msg.getRecipients(RecipientType.BCC), invalids); // convert and validate actual email addresses InternetAddress[] actual = emails2Internets(msg.getRecipients(RecipientType.ACTUAL), invalids); // check that some actual addresses were given. if not, use a compilation of to, cc, bcc if (actual.length == 0) { int total = to.length + cc.length + bcc.length; if (total == 0) { throw new NoRecipientsException( "No valid recipients found on message. Check for invalid email addresses returned from this method."); } actual = new InternetAddress[total]; int count = 0; for (InternetAddress t : to) { actual[count++] = t; } for (InternetAddress c : cc) { actual[count++] = c; } for (InternetAddress b : bcc) { actual[count++] = b; } } // rebundle addresses to expected param type HashMap<RecipientType, InternetAddress[]> headerTo = new HashMap<RecipientType, InternetAddress[]>(); headerTo.put(RecipientType.TO, to); headerTo.put(RecipientType.CC, cc); headerTo.put(RecipientType.BCC, bcc); // convert headers to expected format List<String> headers = msg.extractHeaders(); // build the content type String contentType = EmailHeaders.CONTENT_TYPE + ": " + msg.getContentType(); if (msg.getCharacterSet() != null && msg.getCharacterSet().trim().length() != 0) { contentType += "; charset=" + msg.getCharacterSet(); } // message format is only used when content type is text/plain as specified in the rfc if (ContentType.TEXT_PLAIN.equals(msg.getCharacterSet()) && msg.getFormat() != null && msg.getFormat().trim().length() != 0) { contentType += "; format=" + msg.getFormat(); } // add the content type to the headers headers.add(contentType); // send the message try { sendMailMessagingException(from, actual, msg.getSubject(), msg.getBody(), headerTo, replyTo, headers, msg.getAttachments()); } catch (MessagingException e) { // Just log it, if user doesn't want it thrown if (messagingException == false) { M_log.error("Email.sendMail: exception: " + e.getMessage(), e); } else { throw e; } } return invalids; } /** * Converts a {@link java.util.List} of {@link EmailAddress} to * {@link javax.mail.internet.InternetAddress}. * * @param emails * @return Array will be the same size as the list with converted addresses. If list is null, * the array returned will be 0 length (non-null). * @throws AddressException * @throws UnsupportedEncodingException */ protected InternetAddress[] emails2Internets(List<EmailAddress> emails, List<EmailAddress> invalids) { // set the default return value InternetAddress[] addrs = new InternetAddress[0]; if (emails != null && !emails.isEmpty()) { ArrayList<InternetAddress> laddrs = new ArrayList<InternetAddress>(); for (int i = 0; i < emails.size(); i++) { EmailAddress email = emails.get(i); try { InternetAddress ia = new InternetAddress(email.getAddress(), true); ia.setPersonal(email.getPersonal()); laddrs.add(ia); } catch (AddressException e) { invalids.add(email); } catch (UnsupportedEncodingException e) { invalids.add(email); } } if (!laddrs.isEmpty()) { addrs = laddrs.toArray(addrs); } } return addrs; } protected String cleanUp(String str) { StringBuilder buf = new StringBuilder(str); for (int i = 0; i < buf.length(); i++) { if (buf.charAt(i) == '\n' || buf.charAt(i) == '\r') buf.replace(i, i + 1, " "); } return buf.toString(); } protected String listToStr(Collection list) { if (list == null) return ""; return arrayToStr(list.toArray()); } protected String arrayToStr(Object[] array) { StringBuilder buf = new StringBuilder(); if (array != null) { buf.append("["); for (int i = 0; i < array.length; i++) { if (i != 0) buf.append(", "); buf.append(array[i].toString()); } buf.append("]"); } else { buf.append(""); } return buf.toString(); } /** * Flatten a {@link java.util.Map} to a String * * @param map * @return A string representation of the {@link java.util.Map}. Examples of results include: * Standard key/value pairs: [[key1:value1], [key2:value2]] * List values: [[key1:value1, value2], [key2:value3, value4]] * Map values: [[key1:[key2:value1]], [key3:[key4:value2]]] */ protected String mapToStr(Map map) { StringBuilder sb = new StringBuilder(); if (map != null) { sb.append("["); for (Iterator i = map.entrySet().iterator(); i.hasNext();) { Entry entry = (Entry) i.next(); Object key = entry.getValue(); sb.append("[").append(key).append(":"); Object value = entry.getValue(); if (value instanceof Collection) { sb.append(listToStr((Collection) value)); } else if (value instanceof Object[]) { sb.append(arrayToStr((Object[]) value)); } else if (value instanceof Map) { sb.append(mapToStr((Map) value)); } else { sb.append(value); } sb.append("]"); if (i.hasNext()) sb.append(", "); } sb.append("]"); } return sb.toString(); } protected String usersToStr(Collection<User> users) { StringBuilder buf = new StringBuilder(); buf.append("["); if (users != null) { for (User user : users) { buf.append(user.getDisplayName() + "<" + user.getEmail() + "> "); } } buf.append("]"); return buf.toString(); } /** * Sets the content for a message. Also attaches files to the message. * @throws MessagingException */ protected void setContent(String content, List<Attachment> attachments, MimeMessage msg, String contentType, String charset, String multipartSubtype) throws MessagingException { ArrayList<MimeBodyPart> embeddedAttachments = new ArrayList<MimeBodyPart>(); if (attachments != null && attachments.size() > 0) { // Add attachments to messages for (Attachment attachment : attachments) { // attach the file to the message embeddedAttachments.add(createAttachmentPart(attachment)); } } // if no direct attachments, keep the message simple and add the content as text. if (embeddedAttachments.size() == 0) { // if no contentType specified, go with text/plain if (contentType == null) msg.setText(content, charset); else msg.setContent(content, contentType); } // the multipart was constructed (ie. attachments available), use it as the message content else { // create a multipart container Multipart multipart = (multipartSubtype != null) ? new MimeMultipart(multipartSubtype) : new MimeMultipart(); // create a body part for the message text MimeBodyPart msgBodyPart = new MimeBodyPart(); if (contentType == null) msgBodyPart.setText(content, charset); else msgBodyPart.setContent(content, contentType); // add the message part to the container multipart.addBodyPart(msgBodyPart); // add attachments for (MimeBodyPart attachPart : embeddedAttachments) { multipart.addBodyPart(attachPart); } // set the multipart container as the content of the message msg.setContent(multipart); } } /** * Attaches a file as a body part to the multipart message * * @param attachment * @throws MessagingException */ private MimeBodyPart createAttachmentPart(Attachment attachment) throws MessagingException { DataSource source = attachment.getDataSource(); MimeBodyPart attachPart = new MimeBodyPart(); attachPart.setDataHandler(new DataHandler(source)); attachPart.setFileName(attachment.getFilename()); if (attachment.getContentTypeHeader() != null) { attachPart.setHeader("Content-Type", attachment.getContentTypeHeader()); } if (attachment.getContentDispositionHeader() != null) { attachPart.setHeader("Content-Disposition", attachment.getContentDispositionHeader()); } return attachPart; } /** * test version of sendMail */ protected void testSendMail(InternetAddress from, InternetAddress[] to, String subject, String content, Map<RecipientType, InternetAddress[]> headerTo, InternetAddress[] replyTo, List<String> additionalHeaders) { M_log.info("sendMail: from: " + from + " to: " + arrayToStr(to) + " subject: " + subject + " headerTo: " + mapToStr(headerTo) + " replyTo: " + arrayToStr(replyTo) + " content: " + content + " additionalHeaders: " + listToStr(additionalHeaders)); } /** * test version of send */ protected void testSend(String fromStr, String toStr, String subject, String content, String headerToStr, String replyToStr, List<String> additionalHeaders) { M_log.info("send: from: " + fromStr + " to: " + toStr + " subject: " + subject + " headerTo: " + headerToStr + " replyTo: " + replyToStr + " content: " + content + " additionalHeaders: " + listToStr(additionalHeaders)); } /** Returns true if the given content String can be encoded in the given charset */ protected static boolean canUseCharset(String content, String charsetName) { try { return Charset.forName(charsetName).newEncoder().canEncode(content); } catch (Exception e) { return false; } } protected void sendMessageAndLog(InternetAddress from, InternetAddress[] to, String subject, Map<RecipientType, InternetAddress[]> headerTo, long start, MimeMessage msg, Session session) throws MessagingException { long preSend = 0; if (M_log.isDebugEnabled()) preSend = System.currentTimeMillis(); if (allowTransport) { msg.saveChanges(); Transport transport = session.getTransport(protocol); if (m_smtpUser != null && m_smtpPassword != null) transport.connect(m_smtp, m_smtpUser, m_smtpPassword); else transport.connect(); transport.sendMessage(msg, to); transport.close(); } long end = 0; if (M_log.isDebugEnabled()) end = System.currentTimeMillis(); if (M_log.isInfoEnabled()) { StringBuilder buf = new StringBuilder(); buf.append("Email.sendMail: from: "); buf.append(from); buf.append(" subject: "); buf.append(subject); buf.append(" to:"); for (int i = 0; i < to.length; i++) { buf.append(" "); buf.append(to[i]); } if (headerTo != null) { if (headerTo.containsKey(RecipientType.TO)) { buf.append(" headerTo{to}:"); InternetAddress[] headerToTo = headerTo.get(RecipientType.TO); for (int i = 0; i < headerToTo.length; i++) { buf.append(" "); buf.append(headerToTo[i]); } } if (headerTo.containsKey(RecipientType.CC)) { buf.append(" headerTo{cc}:"); InternetAddress[] headerToCc = headerTo.get(RecipientType.CC); for (int i = 0; i < headerToCc.length; i++) { buf.append(" "); buf.append(headerToCc[i]); } } if (headerTo.containsKey(RecipientType.BCC)) { buf.append(" headerTo{bcc}:"); InternetAddress[] headerToBcc = headerTo.get(RecipientType.BCC); for (int i = 0; i < headerToBcc.length; i++) { buf.append(" "); buf.append(headerToBcc[i]); } } } try { if (msg.getContent() instanceof Multipart) { Multipart parts = (Multipart) msg.getContent(); buf.append(" with ").append(parts.getCount() - 1).append(" attachments"); } } catch (IOException ioe) { } if (M_log.isDebugEnabled()) { buf.append(" time: "); buf.append("" + (end - start)); buf.append(" in send: "); buf.append("" + (end - preSend)); } M_log.info(buf.toString()); } } protected void setRecipients(Map<RecipientType, InternetAddress[]> headerTo, MimeMessage msg) throws MessagingException { if (headerTo != null) { if (msg.getHeader(EmailHeaders.TO) == null && headerTo.containsKey(RecipientType.TO)) { msg.setRecipients(Message.RecipientType.TO, headerTo.get(RecipientType.TO)); } if (msg.getHeader(EmailHeaders.CC) == null && headerTo.containsKey(RecipientType.CC)) { msg.setRecipients(Message.RecipientType.CC, headerTo.get(RecipientType.CC)); } if (headerTo.containsKey(RecipientType.BCC)) { msg.setRecipients(Message.RecipientType.BCC, headerTo.get(RecipientType.BCC)); } } } protected String extractCharset(String contentType) { String charset = null; if (contentType != null) { // try and extract the charset from the Content-Type header int charsetStart = contentType.toLowerCase().indexOf("charset="); if (charsetStart != -1) { int charsetEnd = contentType.indexOf(";", charsetStart); if (charsetEnd == -1) charsetEnd = contentType.length(); charset = contentType.substring(charsetStart + "charset=".length(), charsetEnd).trim(); } } return charset; } /** * inspired by http://java.sun.com/products/javamail/FAQ.html#msgid * * From the FAQ<br> * <p>Q: I set a particular value for the Message-ID header of my new message, but * when I send this message that header is rewritten.</p> * <p>A: A new value for the Message-ID field is * set when the saveChanges method is called (usually implicitly when a message is sent), * overwriting any value you set yourself. If you need to set your own Message-ID and have it * retained, you will have to create your own MimeMessage subclass, override the updateMessageID * method and use an instance of this subclass.</p> */ protected class MyMessage extends MimeMessage { protected String m_id = null; public MyMessage(Session session, String id) { super(session); m_id = id; } public MyMessage(Session session, Collection<String> headers, String message) { super(session); try { // the FULL content-type header, for example: Content-Type: text/plain; charset=windows-1252; format=flowed String contentType = null; // see if we have a message-id: in the headers, or content-type:, otherwise move the headers into the message if (headers != null) { for (String header : headers) { if (header.toLowerCase().startsWith("message-id: ")) { m_id = header.substring(12); } else if (header.toLowerCase().startsWith("content-type: ")) { contentType = header; } else if (header.toLowerCase().startsWith("from: ")) { addEncodedHeader(header, "From: "); } else if (header.toLowerCase().startsWith("to: ")) { addEncodedHeader(header, "To: "); } else if (header.toLowerCase().startsWith("cc: ")) { addEncodedHeader(header, "Cc: "); } else { try { addHeaderLine(header); } catch (MessagingException e) { M_log.warn("Email.MyMessage: exception: " + e.getMessage(), e); } } } } // make sure we have a date, use now if needed if (getHeader("Date") == null) { setSentDate(new Date(System.currentTimeMillis())); } // figure out what charset encoding to use // the character set, for example, windows-1252 or UTF-8 String charset = null; // first try to use the charset from the forwarded Content-Type header (if there is one). // if that charset doesn't work, try a couple others. if (contentType != null) { // try and extract the charset from the Content-Type header int charsetStart = contentType.toLowerCase().indexOf("charset="); if (charsetStart != -1) { int charsetEnd = contentType.indexOf(";", charsetStart); if (charsetEnd == -1) charsetEnd = contentType.length(); charset = contentType.substring(charsetStart + "charset=".length(), charsetEnd).trim(); } } if (charset != null && canUseCharset(message, charset) && canUseCharset(getSubject(), charset)) { // use the charset from the Content-Type header } else if (canUseCharset(message, CharacterSet.ISO_8859_1) && canUseCharset(getSubject(), CharacterSet.ISO_8859_1)) { if (contentType != null && charset != null) contentType = contentType.replaceAll(charset, CharacterSet.ISO_8859_1); else if (contentType != null) contentType += "; charset=" + CharacterSet.ISO_8859_1; charset = CharacterSet.ISO_8859_1; } else if (canUseCharset(message, CharacterSet.WINDOWS_1252) && canUseCharset(getSubject(), CharacterSet.WINDOWS_1252)) { if (contentType != null && charset != null) contentType = contentType.replaceAll(charset, CharacterSet.WINDOWS_1252); else if (contentType != null) contentType += "; charset=" + CharacterSet.WINDOWS_1252; charset = CharacterSet.WINDOWS_1252; } else { // catch-all - UTF-8 should be able to handle anything if (contentType != null && charset != null) contentType = contentType.replaceAll(charset, CharacterSet.UTF_8); else if (contentType != null) contentType += "; charset=" + CharacterSet.UTF_8; else contentType = EmailHeaders.CONTENT_TYPE + ": " + ContentType.TEXT_PLAIN + "; charset=" + CharacterSet.UTF_8; charset = CharacterSet.UTF_8; } if (contentType != null && contentType.contains("multipart/")) { MimeMultipart multiPartContent = new MimeMultipart("alternative"); int indexOfStartOfBoundary = contentType.indexOf("boundary=\"") + 10; String headerStartingWithBoundary = contentType.substring(indexOfStartOfBoundary); String boundary = headerStartingWithBoundary.substring(0, headerStartingWithBoundary.indexOf("\"")); String[] parts = message.split("--" + boundary + "(--)?\n"); // the zeroth part is the line about how this is a MIME message, so we won't use it for (int i = 1; i < parts.length - 1; i++) { String[] partLines = parts[i].split("\n"); StringBuilder partText = new StringBuilder(); for (int j = 1; j < partLines.length; j++) { partText.append(partLines[j] + "\n"); } MimeBodyPart bodyPart = new MimeBodyPart(); String mimeType = partLines[0].contains("text/html") ? "text/html" : "text/plain"; mimeType += "; charset=" + charset; bodyPart.setContent(partText.toString(), mimeType); multiPartContent.addBodyPart(bodyPart); } setContent(multiPartContent); } else { // fill in the body of the message setText(message, charset); } // make sure correct charset is used for subject if (getSubject() != null) setSubject(getSubject(), charset); // if we have a full Content-Type header, set it NOW (after setting the body of the message so that format=flowed is preserved) if (contentType != null && !contentType.contains("multipart/")) { // addHeaderLine("Content-Transfer-Encoding: quoted-printable"); addHeaderLine(contentType); } } catch (MessagingException e) { M_log.warn("Email.MyMessage: exception: " + e.getMessage(), e); } } protected void updateHeaders() throws MessagingException { super.updateHeaders(); if (m_id != null) { setHeader("Message-Id", m_id); } } /** Encode (To,From,Cc) mail headers to safely include UTF-8 characters **/ private void addEncodedHeader(String header, String name) throws MessagingException { try { final String value = header.substring(name.length()); // check for header format that may include UTF-8 characters int index = value.lastIndexOf("<"); if (index == -1) { addHeaderLine(header); } // UTF-8 characters may exists -- encode header string else { if ((index != 0) && (' ' == value.charAt(index - 1))) { index--; } final String title = value.substring(0, index); final String email = value.substring(index); // Accomodate Section 2.1.1 of http://tools.ietf.org/html/rfc2822 (line length should not exceed 78 characters and must not exceed 998) String tempTitle = MimeUtility.encodeText(title, "UTF-8", null); if (name.length() + tempTitle.length() + email.length() > 78) tempTitle = tempTitle.replace(" ", "\n "); final String[] lines = (name + tempTitle + email).split("\r\n|\r|\n"); for (String temp : lines) { addHeaderLine(temp); } } } catch (MessagingException e) { M_log.error("Email.MyMessage: exception: " + e, e); addHeaderLine(header); } catch (UnsupportedEncodingException e) { M_log.error("Email.MyMessage: exception: " + e, e); addHeaderLine(header); } } } public String propName(String propNameTemplate) { return propName(propNameTemplate, PROTOCOL_SMTP); } public String propName(String propNameTemplate, String protocol) { String formattedName = String.format(propNameTemplate, protocol); return formattedName; } }