3 * Copyright (C) 2009-2024 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program 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 * This program 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 License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.server.notification.email;
22 import com.tngtech.java.junit.dataprovider.DataProvider;
23 import com.tngtech.java.junit.dataprovider.DataProviderRunner;
24 import com.tngtech.java.junit.dataprovider.UseDataProvider;
25 import java.io.IOException;
26 import java.util.Collections;
27 import java.util.List;
29 import java.util.Random;
31 import java.util.stream.IntStream;
32 import java.util.stream.Stream;
33 import javax.mail.MessagingException;
34 import javax.mail.internet.MimeMessage;
35 import org.apache.commons.mail.EmailException;
36 import org.junit.After;
37 import org.junit.Before;
38 import org.junit.Rule;
39 import org.junit.Test;
40 import org.junit.runner.RunWith;
41 import org.slf4j.event.Level;
42 import org.sonar.api.config.EmailSettings;
43 import org.sonar.api.notifications.Notification;
44 import org.sonar.api.testfixtures.log.LogTester;
45 import org.sonar.api.utils.log.LoggerLevel;
46 import org.sonar.server.issue.notification.EmailMessage;
47 import org.sonar.server.issue.notification.EmailTemplate;
48 import org.sonar.server.notification.email.EmailNotificationChannel.EmailDeliveryRequest;
49 import org.subethamail.wiser.Wiser;
50 import org.subethamail.wiser.WiserMessage;
52 import static java.util.stream.Collectors.toMap;
53 import static java.util.stream.Collectors.toSet;
54 import static junit.framework.Assert.fail;
55 import static org.apache.commons.lang.RandomStringUtils.random;
56 import static org.assertj.core.api.Assertions.assertThat;
57 import static org.mockito.Mockito.mock;
58 import static org.mockito.Mockito.verify;
59 import static org.mockito.Mockito.verifyNoInteractions;
60 import static org.mockito.Mockito.verifyNoMoreInteractions;
61 import static org.mockito.Mockito.when;
63 @RunWith(DataProviderRunner.class)
64 public class EmailNotificationChannelTest {
66 private static final String SUBJECT_PREFIX = "[SONARQUBE]";
69 public LogTester logTester = new LogTester();
71 private Wiser smtpServer;
72 private EmailSettings configuration;
73 private EmailNotificationChannel underTest;
77 logTester.setLevel(LoggerLevel.DEBUG);
78 smtpServer = new Wiser(0);
81 configuration = mock(EmailSettings.class);
82 underTest = new EmailNotificationChannel(configuration, null, null);
86 public void tearDown() {
91 public void isActivated_returns_true_if_smpt_host_is_not_empty() {
92 when(configuration.getSmtpHost()).thenReturn(random(5));
94 assertThat(underTest.isActivated()).isTrue();
98 public void isActivated_returns_false_if_smpt_host_is_null() {
99 when(configuration.getSmtpHost()).thenReturn(null);
101 assertThat(underTest.isActivated()).isFalse();
105 public void isActivated_returns_false_if_smpt_host_is_empty() {
106 when(configuration.getSmtpHost()).thenReturn("");
108 assertThat(underTest.isActivated()).isFalse();
112 public void shouldSendTestEmail() throws Exception {
114 underTest.sendTestEmail("user@nowhere", "Test Message from SonarQube", "This is a test message from SonarQube.");
116 List<WiserMessage> messages = smtpServer.getMessages();
117 assertThat(messages).hasSize(1);
119 MimeMessage email = messages.get(0).getMimeMessage();
120 assertThat(email.getHeader("Content-Type", null)).isEqualTo("text/plain; charset=UTF-8");
121 assertThat(email.getHeader("From", ",")).isEqualTo("SonarQube from NoWhere <server@nowhere>");
122 assertThat(email.getHeader("To", null)).isEqualTo("<user@nowhere>");
123 assertThat(email.getHeader("Subject", null)).isEqualTo("[SONARQUBE] Test Message from SonarQube");
124 assertThat((String) email.getContent()).startsWith("This is a test message from SonarQube.\r\n\r\nMail sent from: http://nemo.sonarsource.org");
128 public void sendTestEmailShouldSanitizeLog() throws Exception {
129 logTester.setLevel(LoggerLevel.TRACE);
131 underTest.sendTestEmail("user@nowhere", "Test Message from SonarQube", "This is a message \n containing line breaks \r that should be sanitized when logged.");
133 assertThat(logTester.logs(Level.TRACE)).isNotEmpty()
134 .contains("Sending email: This is a message _ containing line breaks _ that should be sanitized when logged.__Mail sent from: http://nemo.sonarsource.org");
139 public void shouldThrowAnExceptionWhenUnableToSendTestEmail() {
144 underTest.sendTestEmail("user@nowhere", "Test Message from SonarQube", "This is a test message from SonarQube.");
146 } catch (EmailException e) {
152 public void shouldNotSendEmailWhenHostnameNotConfigured() {
153 EmailMessage emailMessage = new EmailMessage()
154 .setTo("user@nowhere")
156 .setPlainTextMessage("Bar");
157 boolean delivered = underTest.deliver(emailMessage);
158 assertThat(smtpServer.getMessages()).isEmpty();
159 assertThat(delivered).isFalse();
163 public void shouldSendThreadedEmail() throws Exception {
165 EmailMessage emailMessage = new EmailMessage()
166 .setMessageId("reviews/view/1")
167 .setFrom("Full Username")
168 .setTo("user@nowhere")
169 .setSubject("Review #3")
170 .setPlainTextMessage("I'll take care of this violation.");
171 boolean delivered = underTest.deliver(emailMessage);
173 List<WiserMessage> messages = smtpServer.getMessages();
174 assertThat(messages).hasSize(1);
176 MimeMessage email = messages.get(0).getMimeMessage();
178 assertThat(email.getHeader("Content-Type", null)).isEqualTo("text/plain; charset=UTF-8");
180 assertThat(email.getHeader("In-Reply-To", null)).isEqualTo("<reviews/view/1@nemo.sonarsource.org>");
181 assertThat(email.getHeader("References", null)).isEqualTo("<reviews/view/1@nemo.sonarsource.org>");
183 assertThat(email.getHeader("List-ID", null)).isEqualTo("SonarQube <sonar.nemo.sonarsource.org>");
184 assertThat(email.getHeader("List-Archive", null)).isEqualTo("http://nemo.sonarsource.org");
186 assertThat(email.getHeader("From", ",")).isEqualTo("\"Full Username (SonarQube from NoWhere)\" <server@nowhere>");
187 assertThat(email.getHeader("To", null)).isEqualTo("<user@nowhere>");
188 assertThat(email.getHeader("Subject", null)).isEqualTo("[SONARQUBE] Review #3");
189 assertThat((String) email.getContent()).startsWith("I'll take care of this violation.");
190 assertThat(delivered).isTrue();
194 public void shouldSendNonThreadedEmail() throws Exception {
196 EmailMessage emailMessage = new EmailMessage()
197 .setTo("user@nowhere")
199 .setPlainTextMessage("Bar");
200 boolean delivered = underTest.deliver(emailMessage);
202 List<WiserMessage> messages = smtpServer.getMessages();
203 assertThat(messages).hasSize(1);
205 MimeMessage email = messages.get(0).getMimeMessage();
207 assertThat(email.getHeader("Content-Type", null)).isEqualTo("text/plain; charset=UTF-8");
209 assertThat(email.getHeader("In-Reply-To", null)).isNull();
210 assertThat(email.getHeader("References", null)).isNull();
212 assertThat(email.getHeader("List-ID", null)).isEqualTo("SonarQube <sonar.nemo.sonarsource.org>");
213 assertThat(email.getHeader("List-Archive", null)).isEqualTo("http://nemo.sonarsource.org");
215 assertThat(email.getHeader("From", null)).isEqualTo("SonarQube from NoWhere <server@nowhere>");
216 assertThat(email.getHeader("To", null)).isEqualTo("<user@nowhere>");
217 assertThat(email.getHeader("Subject", null)).isEqualTo("[SONARQUBE] Foo");
218 assertThat((String) email.getContent()).startsWith("Bar");
219 assertThat(delivered).isTrue();
223 public void shouldNotThrowAnExceptionWhenUnableToSendEmail() {
227 EmailMessage emailMessage = new EmailMessage()
228 .setTo("user@nowhere")
230 .setPlainTextMessage("Bar");
231 boolean delivered = underTest.deliver(emailMessage);
233 assertThat(delivered).isFalse();
237 public void shouldSendTestEmailWithSTARTTLS() {
238 smtpServer.getServer().setEnableTLS(true);
239 smtpServer.getServer().setRequireTLS(true);
241 when(configuration.getSecureConnection()).thenReturn("STARTTLS");
244 underTest.sendTestEmail("user@nowhere", "Test Message from SonarQube", "This is a test message from SonarQube.");
245 fail("An SSL exception was expected a a proof that STARTTLS is enabled");
246 } catch (EmailException e) {
247 // We don't have a SSL certificate so we are expecting a SSL error
248 assertThat(e.getCause().getMessage()).isEqualTo("Could not convert socket to TLS");
253 public void deliverAll_has_no_effect_if_set_is_empty() {
254 EmailSettings emailSettings = mock(EmailSettings.class);
255 EmailNotificationChannel underTest = new EmailNotificationChannel(emailSettings, null, null);
257 int count = underTest.deliverAll(Collections.emptySet());
259 assertThat(count).isZero();
260 verifyNoInteractions(emailSettings);
261 assertThat(smtpServer.getMessages()).isEmpty();
265 public void deliverAll_has_no_effect_if_smtp_host_is_null() {
266 EmailSettings emailSettings = mock(EmailSettings.class);
267 when(emailSettings.getSmtpHost()).thenReturn(null);
268 Set<EmailDeliveryRequest> requests = IntStream.range(0, 1 + new Random().nextInt(10))
269 .mapToObj(i -> new EmailDeliveryRequest("foo" + i + "@moo", mock(Notification.class)))
271 EmailNotificationChannel underTest = new EmailNotificationChannel(emailSettings, null, null);
273 int count = underTest.deliverAll(requests);
275 assertThat(count).isZero();
276 verify(emailSettings).getSmtpHost();
277 verifyNoMoreInteractions(emailSettings);
278 assertThat(smtpServer.getMessages()).isEmpty();
282 @UseDataProvider("emptyStrings")
283 public void deliverAll_ignores_requests_which_recipient_is_empty(String emptyString) {
284 EmailSettings emailSettings = mock(EmailSettings.class);
285 when(emailSettings.getSmtpHost()).thenReturn(null);
286 Set<EmailDeliveryRequest> requests = IntStream.range(0, 1 + new Random().nextInt(10))
287 .mapToObj(i -> new EmailDeliveryRequest(emptyString, mock(Notification.class)))
289 EmailNotificationChannel underTest = new EmailNotificationChannel(emailSettings, null, null);
291 int count = underTest.deliverAll(requests);
293 assertThat(count).isZero();
294 verify(emailSettings).getSmtpHost();
295 verifyNoMoreInteractions(emailSettings);
296 assertThat(smtpServer.getMessages()).isEmpty();
300 public void deliverAll_returns_count_of_request_for_which_at_least_one_formatter_accept_it() throws MessagingException, IOException {
301 String recipientEmail = "foo@donut";
303 Notification notification1 = mock(Notification.class);
304 Notification notification2 = mock(Notification.class);
305 Notification notification3 = mock(Notification.class);
306 EmailTemplate template1 = mock(EmailTemplate.class);
307 EmailTemplate template3 = mock(EmailTemplate.class);
308 EmailMessage emailMessage1 = new EmailMessage().setTo(recipientEmail).setSubject("sub11").setPlainTextMessage("msg11");
309 EmailMessage emailMessage3 = new EmailMessage().setTo(recipientEmail).setSubject("sub3").setPlainTextMessage("msg3");
310 when(template1.format(notification1)).thenReturn(emailMessage1);
311 when(template3.format(notification3)).thenReturn(emailMessage3);
312 Set<EmailDeliveryRequest> requests = Stream.of(notification1, notification2, notification3)
313 .map(t -> new EmailDeliveryRequest(recipientEmail, t))
315 EmailNotificationChannel underTest = new EmailNotificationChannel(configuration, new EmailTemplate[] {template1, template3}, null);
317 int count = underTest.deliverAll(requests);
319 assertThat(count).isEqualTo(2);
320 assertThat(smtpServer.getMessages()).hasSize(2);
321 Map<String, MimeMessage> messagesBySubject = smtpServer.getMessages().stream()
324 return t.getMimeMessage();
325 } catch (MessagingException e) {
326 throw new RuntimeException(e);
329 .collect(toMap(t -> {
331 return t.getSubject();
332 } catch (MessagingException e) {
333 throw new RuntimeException(e);
337 assertThat((String) messagesBySubject.get(SUBJECT_PREFIX + " " + emailMessage1.getSubject()).getContent())
338 .contains(emailMessage1.getMessage());
339 assertThat((String) messagesBySubject.get(SUBJECT_PREFIX + " " + emailMessage3.getSubject()).getContent())
340 .contains(emailMessage3.getMessage());
344 public void deliverAll_ignores_multiple_templates_by_notification_and_takes_the_first_one_only() throws MessagingException, IOException {
345 String recipientEmail = "foo@donut";
347 Notification notification1 = mock(Notification.class);
348 EmailTemplate template11 = mock(EmailTemplate.class);
349 EmailTemplate template12 = mock(EmailTemplate.class);
350 EmailMessage emailMessage11 = new EmailMessage().setTo(recipientEmail).setSubject("sub11").setPlainTextMessage("msg11");
351 EmailMessage emailMessage12 = new EmailMessage().setTo(recipientEmail).setSubject("sub12").setPlainTextMessage("msg12");
352 when(template11.format(notification1)).thenReturn(emailMessage11);
353 when(template12.format(notification1)).thenReturn(emailMessage12);
354 EmailDeliveryRequest request = new EmailDeliveryRequest(recipientEmail, notification1);
355 EmailNotificationChannel underTest = new EmailNotificationChannel(configuration, new EmailTemplate[] {template11, template12}, null);
357 int count = underTest.deliverAll(Collections.singleton(request));
359 assertThat(count).isOne();
360 assertThat(smtpServer.getMessages()).hasSize(1);
361 assertThat((String) smtpServer.getMessages().iterator().next().getMimeMessage().getContent())
362 .contains(emailMessage11.getMessage());
366 public static Object[][] emptyStrings() {
367 return new Object[][] {
374 private void configure() {
375 when(configuration.getSmtpHost()).thenReturn("localhost");
376 when(configuration.getSmtpPort()).thenReturn(smtpServer.getServer().getPort());
377 when(configuration.getFrom()).thenReturn("server@nowhere");
378 when(configuration.getFromName()).thenReturn("SonarQube from NoWhere");
379 when(configuration.getPrefix()).thenReturn(SUBJECT_PREFIX);
380 when(configuration.getServerBaseURL()).thenReturn("http://nemo.sonarsource.org");