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 */
020 package org.sonar.plugins.emailnotifications;
021
022 import java.net.MalformedURLException;
023 import java.net.URL;
024
025 import org.apache.commons.lang.StringUtils;
026 import org.apache.commons.mail.EmailException;
027 import org.apache.commons.mail.SimpleEmail;
028 import org.slf4j.Logger;
029 import org.slf4j.LoggerFactory;
030 import org.sonar.api.database.model.User;
031 import org.sonar.api.notifications.Notification;
032 import org.sonar.api.notifications.NotificationChannel;
033 import org.sonar.api.security.UserFinder;
034 import org.sonar.api.utils.SonarException;
035 import org.sonar.plugins.emailnotifications.api.EmailMessage;
036 import 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 */
048 public 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 }