001/* 002 * Sonar, open source software quality management tool. 003 * Copyright (C) 2008-2012 SonarSource 004 * mailto:contact AT sonarsource DOT com 005 * 006 * Sonar is free software; you can redistribute it and/or 007 * modify it under the terms of the GNU Lesser General Public 008 * License as published by the Free Software Foundation; either 009 * version 3 of the License, or (at your option) any later version. 010 * 011 * Sonar is distributed in the hope that it will be useful, 012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 014 * Lesser General Public License for more details. 015 * 016 * You should have received a copy of the GNU Lesser General Public 017 * License along with Sonar; if not, write to the Free Software 018 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 019 */ 020package org.sonar.plugins.emailnotifications; 021 022import java.net.MalformedURLException; 023import java.net.URL; 024 025import org.apache.commons.lang.StringUtils; 026import org.apache.commons.mail.EmailException; 027import org.apache.commons.mail.SimpleEmail; 028import org.slf4j.Logger; 029import org.slf4j.LoggerFactory; 030import org.sonar.api.database.model.User; 031import org.sonar.api.notifications.Notification; 032import org.sonar.api.notifications.NotificationChannel; 033import org.sonar.api.security.UserFinder; 034import org.sonar.api.utils.SonarException; 035import org.sonar.plugins.emailnotifications.api.EmailMessage; 036import org.sonar.plugins.emailnotifications.api.EmailTemplate; 037 038/** 039 * References: 040 * <ul> 041 * <li><a href="http://tools.ietf.org/html/rfc4021">Registration of Mail and MIME Header Fields</a></li> 042 * <li><a href="http://tools.ietf.org/html/rfc2919">List-Id: A Structured Field and Namespace for the Identification of Mailing Lists</a></li> 043 * <li><a href="https://github.com/blog/798-threaded-email-notifications">GitHub: Threaded Email Notifications</a></li> 044 * </ul> 045 * 046 * @since 2.10 047 */ 048public class EmailNotificationChannel extends NotificationChannel { 049 050 private static final Logger LOG = LoggerFactory.getLogger(EmailNotificationChannel.class); 051 052 /** 053 * @see org.apache.commons.mail.Email#setSocketConnectionTimeout(int) 054 * @see org.apache.commons.mail.Email#setSocketTimeout(int) 055 */ 056 private static final int SOCKET_TIMEOUT = 30000; 057 058 /** 059 * Email Header Field: "List-ID". 060 * Value of this field should contain mailing list identifier as specified in <a href="http://tools.ietf.org/html/rfc2919">RFC 2919</a>. 061 */ 062 private static final String LIST_ID_HEADER = "List-ID"; 063 064 /** 065 * Email Header Field: "List-Archive". 066 * Value of this field should contain URL of mailing list archive as specified in <a href="http://tools.ietf.org/html/rfc2369">RFC 2369</a>. 067 */ 068 private static final String LIST_ARCHIVE_HEADER = "List-Archive"; 069 070 /** 071 * Email Header Field: "In-Reply-To". 072 * Value of this field should contain related message identifier as specified in <a href="http://tools.ietf.org/html/rfc2822">RFC 2822</a>. 073 */ 074 private static final String IN_REPLY_TO_HEADER = "In-Reply-To"; 075 076 /** 077 * Email Header Field: "References". 078 * Value of this field should contain related message identifier as specified in <a href="http://tools.ietf.org/html/rfc2822">RFC 2822</a> 079 */ 080 private static final String REFERENCES_HEADER = "References"; 081 082 private static final String FROM_NAME_DEFAULT = "Sonar"; 083 private static final String SUBJECT_DEFAULT = "Notification"; 084 085 private EmailConfiguration configuration; 086 private EmailTemplate[] templates; 087 private UserFinder userFinder; 088 089 public EmailNotificationChannel(EmailConfiguration configuration, EmailTemplate[] templates, UserFinder userFinder) { 090 this.configuration = configuration; 091 this.templates = templates; 092 this.userFinder = userFinder; 093 } 094 095 @Override 096 public void deliver(Notification notification, String username) { 097 User user = userFinder.findByLogin(username); 098 if (StringUtils.isBlank(user.getEmail())) { 099 LOG.debug("Email not defined for user: " + username); 100 return; 101 } 102 EmailMessage emailMessage = format(notification); 103 if (emailMessage != null) { 104 emailMessage.setTo(user.getEmail()); 105 deliver(emailMessage); 106 } 107 } 108 109 private EmailMessage format(Notification notification) { 110 for (EmailTemplate template : templates) { 111 EmailMessage email = template.format(notification); 112 if (email != null) { 113 return email; 114 } 115 } 116 LOG.warn("Email template not found for notification: {}", notification); 117 return null; 118 } 119 120 /** 121 * Visibility has been relaxed for tests. 122 */ 123 void deliver(EmailMessage emailMessage) { 124 if (StringUtils.isBlank(configuration.getSmtpHost())) { 125 LOG.debug("SMTP host was not configured - email will not be sent"); 126 return; 127 } 128 try { 129 send(emailMessage); 130 } catch (EmailException e) { 131 LOG.error("Unable to send email", e); 132 } 133 } 134 135 private void send(EmailMessage emailMessage) throws EmailException { 136 // Trick to correctly initilize javax.mail library 137 ClassLoader classloader = Thread.currentThread().getContextClassLoader(); 138 Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); 139 140 try { 141 LOG.debug("Sending email: {}", emailMessage); 142 String host = null; 143 try { 144 host = new URL(configuration.getServerBaseURL()).getHost(); 145 } catch (MalformedURLException e) { 146 // ignore 147 } 148 149 SimpleEmail email = new SimpleEmail(); 150 if (StringUtils.isNotBlank(host)) { 151 /* 152 * Set headers for proper threading: GMail will not group messages, even if they have same subject, but don't have "In-Reply-To" and 153 * "References" headers. TODO investigate threading in other clients like KMail, Thunderbird, Outlook 154 */ 155 if (StringUtils.isNotEmpty(emailMessage.getMessageId())) { 156 String messageId = "<" + emailMessage.getMessageId() + "@" + host + ">"; 157 email.addHeader(IN_REPLY_TO_HEADER, messageId); 158 email.addHeader(REFERENCES_HEADER, messageId); 159 } 160 // Set headers for proper filtering 161 email.addHeader(LIST_ID_HEADER, "Sonar <sonar." + host + ">"); 162 email.addHeader(LIST_ARCHIVE_HEADER, configuration.getServerBaseURL()); 163 } 164 // Set general information 165 email.setCharset("UTF-8"); 166 String from = StringUtils.isBlank(emailMessage.getFrom()) ? FROM_NAME_DEFAULT : emailMessage.getFrom() + " (Sonar)"; 167 email.setFrom(configuration.getFrom(), from); 168 email.addTo(emailMessage.getTo(), " "); 169 String subject = StringUtils.defaultIfBlank(StringUtils.trimToEmpty(configuration.getPrefix()) + " ", "") 170 + StringUtils.defaultString(emailMessage.getSubject(), SUBJECT_DEFAULT); 171 email.setSubject(subject); 172 email.setMsg(emailMessage.getMessage()); 173 // Send 174 email.setHostName(configuration.getSmtpHost()); 175 if (StringUtils.equalsIgnoreCase(configuration.getSecureConnection(), "SSL")) { 176 email.setSSL(true); 177 email.setSslSmtpPort(configuration.getSmtpPort()); 178 179 // this port is not used except in EmailException message, that's why it's set with the same value than SSL port. 180 // It prevents from getting bad message. 181 email.setSmtpPort(Integer.parseInt(configuration.getSmtpPort())); 182 } else if (StringUtils.isBlank(configuration.getSecureConnection())) { 183 email.setSmtpPort(Integer.parseInt(configuration.getSmtpPort())); 184 } else { 185 throw new SonarException("Unknown type of SMTP secure connection: " + configuration.getSecureConnection()); 186 } 187 if (StringUtils.isNotBlank(configuration.getSmtpUsername()) || StringUtils.isNotBlank(configuration.getSmtpPassword())) { 188 email.setAuthentication(configuration.getSmtpUsername(), configuration.getSmtpPassword()); 189 } 190 email.setSocketConnectionTimeout(SOCKET_TIMEOUT); 191 email.setSocketTimeout(SOCKET_TIMEOUT); 192 email.send(); 193 194 } finally { 195 Thread.currentThread().setContextClassLoader(classloader); 196 } 197 } 198 199 /** 200 * Send test email. This method called from Ruby. 201 * 202 * @throws EmailException when unable to send 203 */ 204 public void sendTestEmail(String toAddress, String subject, String message) throws EmailException { 205 try { 206 EmailMessage emailMessage = new EmailMessage(); 207 emailMessage.setTo(toAddress); 208 emailMessage.setSubject(subject); 209 emailMessage.setMessage(message); 210 send(emailMessage); 211 } catch (EmailException e) { 212 LOG.error("Fail to send test email to: " + toAddress, e); 213 throw e; 214 } 215 } 216 217}