2 * Sonar, open source software quality management tool.
3 * Copyright (C) 2008-2012 SonarSource
4 * mailto:contact AT sonarsource DOT com
6 * Sonar is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * Sonar is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public
17 * License along with Sonar; if not, write to the Free Software
18 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02
20 package org.sonar.plugins.emailnotifications;
22 import org.apache.commons.lang.StringUtils;
23 import org.apache.commons.mail.EmailException;
24 import org.apache.commons.mail.SimpleEmail;
25 import org.slf4j.Logger;
26 import org.slf4j.LoggerFactory;
27 import org.sonar.api.database.model.User;
28 import org.sonar.api.notifications.Notification;
29 import org.sonar.api.notifications.NotificationChannel;
30 import org.sonar.api.platform.EmailSettings;
31 import org.sonar.api.security.UserFinder;
32 import org.sonar.api.utils.SonarException;
33 import org.sonar.plugins.emailnotifications.api.EmailMessage;
34 import org.sonar.plugins.emailnotifications.api.EmailTemplate;
36 import java.net.MalformedURLException;
42 * <li><a href="http://tools.ietf.org/html/rfc4021">Registration of Mail and MIME Header Fields</a></li>
43 * <li><a href="http://tools.ietf.org/html/rfc2919">List-Id: A Structured Field and Namespace for the Identification of Mailing Lists</a></li>
44 * <li><a href="https://github.com/blog/798-threaded-email-notifications">GitHub: Threaded Email Notifications</a></li>
49 public class EmailNotificationChannel extends NotificationChannel {
51 private static final Logger LOG = LoggerFactory.getLogger(EmailNotificationChannel.class);
54 * @see org.apache.commons.mail.Email#setSocketConnectionTimeout(int)
55 * @see org.apache.commons.mail.Email#setSocketTimeout(int)
57 private static final int SOCKET_TIMEOUT = 30000;
60 * Email Header Field: "List-ID".
61 * Value of this field should contain mailing list identifier as specified in <a href="http://tools.ietf.org/html/rfc2919">RFC 2919</a>.
63 private static final String LIST_ID_HEADER = "List-ID";
66 * Email Header Field: "List-Archive".
67 * 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>.
69 private static final String LIST_ARCHIVE_HEADER = "List-Archive";
72 * Email Header Field: "In-Reply-To".
73 * Value of this field should contain related message identifier as specified in <a href="http://tools.ietf.org/html/rfc2822">RFC 2822</a>.
75 private static final String IN_REPLY_TO_HEADER = "In-Reply-To";
78 * Email Header Field: "References".
79 * Value of this field should contain related message identifier as specified in <a href="http://tools.ietf.org/html/rfc2822">RFC 2822</a>
81 private static final String REFERENCES_HEADER = "References";
83 private static final String FROM_NAME_DEFAULT = "Sonar";
84 private static final String SUBJECT_DEFAULT = "Notification";
86 private EmailSettings configuration;
87 private EmailTemplate[] templates;
88 private UserFinder userFinder;
90 public EmailNotificationChannel(EmailSettings configuration, EmailTemplate[] templates, UserFinder userFinder) {
91 this.configuration = configuration;
92 this.templates = templates;
93 this.userFinder = userFinder;
97 public void deliver(Notification notification, String username) {
98 User user = userFinder.findByLogin(username);
99 if (StringUtils.isBlank(user.getEmail())) {
100 LOG.debug("Email not defined for user: " + username);
103 EmailMessage emailMessage = format(notification);
104 if (emailMessage != null) {
105 emailMessage.setTo(user.getEmail());
106 deliver(emailMessage);
110 private EmailMessage format(Notification notification) {
111 for (EmailTemplate template : templates) {
112 EmailMessage email = template.format(notification);
117 LOG.warn("Email template not found for notification: {}", notification);
122 * Visibility has been relaxed for tests.
124 void deliver(EmailMessage emailMessage) {
125 if (StringUtils.isBlank(configuration.getSmtpHost())) {
126 LOG.debug("SMTP host was not configured - email will not be sent");
131 } catch (EmailException e) {
132 LOG.error("Unable to send email", e);
136 private void send(EmailMessage emailMessage) throws EmailException {
137 // Trick to correctly initilize javax.mail library
138 ClassLoader classloader = Thread.currentThread().getContextClassLoader();
139 Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
142 LOG.debug("Sending email: {}", emailMessage);
145 host = new URL(configuration.getServerBaseURL()).getHost();
146 } catch (MalformedURLException e) {
150 SimpleEmail email = new SimpleEmail();
151 if (StringUtils.isNotBlank(host)) {
153 * Set headers for proper threading: GMail will not group messages, even if they have same subject, but don't have "In-Reply-To" and
154 * "References" headers. TODO investigate threading in other clients like KMail, Thunderbird, Outlook
156 if (StringUtils.isNotEmpty(emailMessage.getMessageId())) {
157 String messageId = "<" + emailMessage.getMessageId() + "@" + host + ">";
158 email.addHeader(IN_REPLY_TO_HEADER, messageId);
159 email.addHeader(REFERENCES_HEADER, messageId);
161 // Set headers for proper filtering
162 email.addHeader(LIST_ID_HEADER, "Sonar <sonar." + host + ">");
163 email.addHeader(LIST_ARCHIVE_HEADER, configuration.getServerBaseURL());
165 // Set general information
166 email.setCharset("UTF-8");
167 String from = StringUtils.isBlank(emailMessage.getFrom()) ? FROM_NAME_DEFAULT : emailMessage.getFrom() + " (Sonar)";
168 email.setFrom(configuration.getFrom(), from);
169 email.addTo(emailMessage.getTo(), " ");
170 String subject = StringUtils.defaultIfBlank(StringUtils.trimToEmpty(configuration.getPrefix()) + " ", "")
171 + StringUtils.defaultString(emailMessage.getSubject(), SUBJECT_DEFAULT);
172 email.setSubject(subject);
173 email.setMsg(emailMessage.getMessage());
175 email.setHostName(configuration.getSmtpHost());
176 if (StringUtils.equalsIgnoreCase(configuration.getSecureConnection(), "SSL")) {
178 email.setSslSmtpPort(configuration.getSmtpPort());
180 // this port is not used except in EmailException message, that's why it's set with the same value than SSL port.
181 // It prevents from getting bad message.
182 email.setSmtpPort(Integer.parseInt(configuration.getSmtpPort()));
183 } else if (StringUtils.isBlank(configuration.getSecureConnection())) {
184 email.setSmtpPort(Integer.parseInt(configuration.getSmtpPort()));
186 throw new SonarException("Unknown type of SMTP secure connection: " + configuration.getSecureConnection());
188 if (StringUtils.isNotBlank(configuration.getSmtpUsername()) || StringUtils.isNotBlank(configuration.getSmtpPassword())) {
189 email.setAuthentication(configuration.getSmtpUsername(), configuration.getSmtpPassword());
191 email.setSocketConnectionTimeout(SOCKET_TIMEOUT);
192 email.setSocketTimeout(SOCKET_TIMEOUT);
196 Thread.currentThread().setContextClassLoader(classloader);
201 * Send test email. This method called from Ruby.
203 * @throws EmailException when unable to send
205 public void sendTestEmail(String toAddress, String subject, String message) throws EmailException {
207 EmailMessage emailMessage = new EmailMessage();
208 emailMessage.setTo(toAddress);
209 emailMessage.setSubject(subject);
210 emailMessage.setMessage(message);
212 } catch (EmailException e) {
213 LOG.error("Fail to send test email to: " + toAddress, e);