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.notifications.Notification;
43 import org.sonar.api.platform.Server;
44 import org.sonar.api.testfixtures.log.LogTester;
45 import org.sonar.api.utils.log.LoggerLevel;
46 import org.sonar.server.email.EmailSmtpConfiguration;
47 import org.sonar.server.issue.notification.EmailMessage;
48 import org.sonar.server.issue.notification.EmailTemplate;
49 import org.sonar.server.notification.email.EmailNotificationChannel.EmailDeliveryRequest;
50 import org.subethamail.wiser.Wiser;
51 import org.subethamail.wiser.WiserMessage;
53 import static java.util.stream.Collectors.toMap;
54 import static java.util.stream.Collectors.toSet;
55 import static junit.framework.Assert.fail;
56 import static org.apache.commons.lang3.RandomStringUtils.random;
57 import static org.assertj.core.api.Assertions.assertThat;
58 import static org.mockito.Mockito.mock;
59 import static org.mockito.Mockito.verify;
60 import static org.mockito.Mockito.verifyNoInteractions;
61 import static org.mockito.Mockito.verifyNoMoreInteractions;
62 import static org.mockito.Mockito.when;
64 @RunWith(DataProviderRunner.class)
65 public class EmailNotificationChannelTest {
67 private static final String SUBJECT_PREFIX = "[SONARQUBE]";
70 public LogTester logTester = new LogTester();
72 private Wiser smtpServer;
73 private EmailSmtpConfiguration configuration;
74 private Server server;
75 private EmailNotificationChannel underTest;
79 logTester.setLevel(LoggerLevel.DEBUG);
80 smtpServer = new Wiser(0);
83 configuration = mock(EmailSmtpConfiguration.class);
84 server = mock(Server.class);
86 underTest = new EmailNotificationChannel(configuration, server, null, null);
90 public void tearDown() {
95 public void isActivated_returns_true_if_smpt_host_is_not_empty() {
96 when(configuration.getSmtpHost()).thenReturn(random(5));
98 assertThat(underTest.isActivated()).isTrue();
102 public void isActivated_returns_false_if_smpt_host_is_null() {
103 when(configuration.getSmtpHost()).thenReturn(null);
105 assertThat(underTest.isActivated()).isFalse();
109 public void isActivated_returns_false_if_smpt_host_is_empty() {
110 when(configuration.getSmtpHost()).thenReturn("");
112 assertThat(underTest.isActivated()).isFalse();
116 public void shouldSendTestEmail() throws Exception {
118 underTest.sendTestEmail("user@nowhere", "Test Message from SonarQube", "This is a test message from SonarQube.");
120 List<WiserMessage> messages = smtpServer.getMessages();
121 assertThat(messages).hasSize(1);
123 MimeMessage email = messages.get(0).getMimeMessage();
124 assertThat(email.getHeader("Content-Type", null)).isEqualTo("text/plain; charset=UTF-8");
125 assertThat(email.getHeader("From", ",")).isEqualTo("SonarQube from NoWhere <server@nowhere>");
126 assertThat(email.getHeader("To", null)).isEqualTo("<user@nowhere>");
127 assertThat(email.getHeader("Subject", null)).isEqualTo("[SONARQUBE] Test Message from SonarQube");
128 assertThat((String) email.getContent()).startsWith("This is a test message from SonarQube.\r\n\r\nMail sent from: http://nemo.sonarsource.org");
132 public void sendTestEmailShouldSanitizeLog() throws Exception {
133 logTester.setLevel(LoggerLevel.TRACE);
135 underTest.sendTestEmail("user@nowhere", "Test Message from SonarQube", "This is a message \n containing line breaks \r that should be sanitized when logged.");
137 assertThat(logTester.logs(Level.TRACE)).isNotEmpty()
138 .contains("Sending email: This is a message _ containing line breaks _ that should be sanitized when logged.__Mail sent from: http://nemo.sonarsource.org");
143 public void shouldThrowAnExceptionWhenUnableToSendTestEmail() {
148 underTest.sendTestEmail("user@nowhere", "Test Message from SonarQube", "This is a test message from SonarQube.");
150 } catch (EmailException e) {
156 public void shouldNotSendEmailWhenHostnameNotConfigured() {
157 EmailMessage emailMessage = new EmailMessage()
158 .setTo("user@nowhere")
160 .setPlainTextMessage("Bar");
161 boolean delivered = underTest.deliver(emailMessage);
162 assertThat(smtpServer.getMessages()).isEmpty();
163 assertThat(delivered).isFalse();
167 public void shouldSendThreadedEmail() throws Exception {
169 EmailMessage emailMessage = new EmailMessage()
170 .setMessageId("reviews/view/1")
171 .setFrom("Full Username")
172 .setTo("user@nowhere")
173 .setSubject("Review #3")
174 .setPlainTextMessage("I'll take care of this violation.");
175 boolean delivered = underTest.deliver(emailMessage);
177 List<WiserMessage> messages = smtpServer.getMessages();
178 assertThat(messages).hasSize(1);
180 MimeMessage email = messages.get(0).getMimeMessage();
182 assertThat(email.getHeader("Content-Type", null)).isEqualTo("text/plain; charset=UTF-8");
184 assertThat(email.getHeader("In-Reply-To", null)).isEqualTo("<reviews/view/1@nemo.sonarsource.org>");
185 assertThat(email.getHeader("References", null)).isEqualTo("<reviews/view/1@nemo.sonarsource.org>");
187 assertThat(email.getHeader("List-ID", null)).isEqualTo("SonarQube <sonar.nemo.sonarsource.org>");
188 assertThat(email.getHeader("List-Archive", null)).isEqualTo("http://nemo.sonarsource.org");
190 assertThat(email.getHeader("From", ",")).isEqualTo("\"Full Username (SonarQube from NoWhere)\" <server@nowhere>");
191 assertThat(email.getHeader("To", null)).isEqualTo("<user@nowhere>");
192 assertThat(email.getHeader("Subject", null)).isEqualTo("[SONARQUBE] Review #3");
193 assertThat((String) email.getContent()).startsWith("I'll take care of this violation.");
194 assertThat(delivered).isTrue();
198 public void shouldSendNonThreadedEmail() throws Exception {
200 EmailMessage emailMessage = new EmailMessage()
201 .setTo("user@nowhere")
203 .setPlainTextMessage("Bar");
204 boolean delivered = underTest.deliver(emailMessage);
206 List<WiserMessage> messages = smtpServer.getMessages();
207 assertThat(messages).hasSize(1);
209 MimeMessage email = messages.get(0).getMimeMessage();
211 assertThat(email.getHeader("Content-Type", null)).isEqualTo("text/plain; charset=UTF-8");
213 assertThat(email.getHeader("In-Reply-To", null)).isNull();
214 assertThat(email.getHeader("References", null)).isNull();
216 assertThat(email.getHeader("List-ID", null)).isEqualTo("SonarQube <sonar.nemo.sonarsource.org>");
217 assertThat(email.getHeader("List-Archive", null)).isEqualTo("http://nemo.sonarsource.org");
219 assertThat(email.getHeader("From", null)).isEqualTo("SonarQube from NoWhere <server@nowhere>");
220 assertThat(email.getHeader("To", null)).isEqualTo("<user@nowhere>");
221 assertThat(email.getHeader("Subject", null)).isEqualTo("[SONARQUBE] Foo");
222 assertThat((String) email.getContent()).startsWith("Bar");
223 assertThat(delivered).isTrue();
227 public void shouldNotThrowAnExceptionWhenUnableToSendEmail() {
231 EmailMessage emailMessage = new EmailMessage()
232 .setTo("user@nowhere")
234 .setPlainTextMessage("Bar");
235 boolean delivered = underTest.deliver(emailMessage);
237 assertThat(delivered).isFalse();
241 public void shouldSendTestEmailWithSTARTTLS() {
242 smtpServer.getServer().setEnableTLS(true);
243 smtpServer.getServer().setRequireTLS(true);
245 when(configuration.getSecureConnection()).thenReturn("STARTTLS");
248 underTest.sendTestEmail("user@nowhere", "Test Message from SonarQube", "This is a test message from SonarQube.");
249 fail("An SSL exception was expected a a proof that STARTTLS is enabled");
250 } catch (EmailException e) {
251 // We don't have a SSL certificate so we are expecting a SSL error
252 assertThat(e.getCause().getMessage()).isEqualTo("Could not convert socket to TLS");
257 public void deliverAll_has_no_effect_if_set_is_empty() {
258 EmailSmtpConfiguration emailSettings = mock(EmailSmtpConfiguration.class);
259 EmailNotificationChannel emailNotificationChannel = new EmailNotificationChannel(emailSettings, server, null, null);
261 int count = emailNotificationChannel.deliverAll(Collections.emptySet());
263 assertThat(count).isZero();
264 verifyNoInteractions(emailSettings);
265 assertThat(smtpServer.getMessages()).isEmpty();
269 public void deliverAll_has_no_effect_if_smtp_host_is_null() {
270 EmailSmtpConfiguration emailSettings = mock(EmailSmtpConfiguration.class);
271 when(emailSettings.getSmtpHost()).thenReturn(null);
272 Set<EmailDeliveryRequest> requests = IntStream.range(0, 1 + new Random().nextInt(10))
273 .mapToObj(i -> new EmailDeliveryRequest("foo" + i + "@moo", mock(Notification.class)))
275 EmailNotificationChannel emailNotificationChannel = new EmailNotificationChannel(emailSettings, server, null, null);
277 int count = emailNotificationChannel.deliverAll(requests);
279 assertThat(count).isZero();
280 verify(emailSettings).getSmtpHost();
281 verifyNoMoreInteractions(emailSettings);
282 assertThat(smtpServer.getMessages()).isEmpty();
286 @UseDataProvider("emptyStrings")
287 public void deliverAll_ignores_requests_which_recipient_is_empty(String emptyString) {
288 EmailSmtpConfiguration emailSettings = mock(EmailSmtpConfiguration.class);
289 when(emailSettings.getSmtpHost()).thenReturn(null);
290 Set<EmailDeliveryRequest> requests = IntStream.range(0, 1 + new Random().nextInt(10))
291 .mapToObj(i -> new EmailDeliveryRequest(emptyString, mock(Notification.class)))
293 EmailNotificationChannel emailNotificationChannel = new EmailNotificationChannel(emailSettings, server, null, null);
295 int count = emailNotificationChannel.deliverAll(requests);
297 assertThat(count).isZero();
298 verify(emailSettings).getSmtpHost();
299 verifyNoMoreInteractions(emailSettings);
300 assertThat(smtpServer.getMessages()).isEmpty();
304 public void deliverAll_returns_count_of_request_for_which_at_least_one_formatter_accept_it() throws MessagingException, IOException {
305 String recipientEmail = "foo@donut";
307 Notification notification1 = mock(Notification.class);
308 Notification notification2 = mock(Notification.class);
309 Notification notification3 = mock(Notification.class);
310 EmailTemplate template1 = mock(EmailTemplate.class);
311 EmailTemplate template3 = mock(EmailTemplate.class);
312 EmailMessage emailMessage1 = new EmailMessage().setTo(recipientEmail).setSubject("sub11").setPlainTextMessage("msg11");
313 EmailMessage emailMessage3 = new EmailMessage().setTo(recipientEmail).setSubject("sub3").setPlainTextMessage("msg3");
314 when(template1.format(notification1)).thenReturn(emailMessage1);
315 when(template3.format(notification3)).thenReturn(emailMessage3);
316 Set<EmailDeliveryRequest> requests = Stream.of(notification1, notification2, notification3)
317 .map(t -> new EmailDeliveryRequest(recipientEmail, t))
319 EmailNotificationChannel emailNotificationChannel = new EmailNotificationChannel(configuration, server, new EmailTemplate[] {template1, template3}, null);
321 int count = emailNotificationChannel.deliverAll(requests);
323 assertThat(count).isEqualTo(2);
324 assertThat(smtpServer.getMessages()).hasSize(2);
325 Map<String, MimeMessage> messagesBySubject = smtpServer.getMessages().stream()
328 return t.getMimeMessage();
329 } catch (MessagingException e) {
330 throw new RuntimeException(e);
333 .collect(toMap(t -> {
335 return t.getSubject();
336 } catch (MessagingException e) {
337 throw new RuntimeException(e);
341 assertThat((String) messagesBySubject.get(SUBJECT_PREFIX + " " + emailMessage1.getSubject()).getContent())
342 .contains(emailMessage1.getMessage());
343 assertThat((String) messagesBySubject.get(SUBJECT_PREFIX + " " + emailMessage3.getSubject()).getContent())
344 .contains(emailMessage3.getMessage());
348 public void deliverAll_ignores_multiple_templates_by_notification_and_takes_the_first_one_only() throws MessagingException, IOException {
349 String recipientEmail = "foo@donut";
351 Notification notification1 = mock(Notification.class);
352 EmailTemplate template11 = mock(EmailTemplate.class);
353 EmailTemplate template12 = mock(EmailTemplate.class);
354 EmailMessage emailMessage11 = new EmailMessage().setTo(recipientEmail).setSubject("sub11").setPlainTextMessage("msg11");
355 EmailMessage emailMessage12 = new EmailMessage().setTo(recipientEmail).setSubject("sub12").setPlainTextMessage("msg12");
356 when(template11.format(notification1)).thenReturn(emailMessage11);
357 when(template12.format(notification1)).thenReturn(emailMessage12);
358 EmailDeliveryRequest request = new EmailDeliveryRequest(recipientEmail, notification1);
359 EmailNotificationChannel emailNotificationChannel = new EmailNotificationChannel(configuration, server, new EmailTemplate[] {template11, template12}, null);
361 int count = emailNotificationChannel.deliverAll(Collections.singleton(request));
363 assertThat(count).isOne();
364 assertThat(smtpServer.getMessages()).hasSize(1);
365 assertThat((String) smtpServer.getMessages().iterator().next().getMimeMessage().getContent())
366 .contains(emailMessage11.getMessage());
370 public static Object[][] emptyStrings() {
371 return new Object[][] {
378 private void configure() {
379 when(configuration.getSmtpHost()).thenReturn("localhost");
380 when(configuration.getSmtpPort()).thenReturn(smtpServer.getServer().getPort());
381 when(configuration.getSecureConnection()).thenReturn("NONE");
382 when(configuration.getFrom()).thenReturn("server@nowhere");
383 when(configuration.getFromName()).thenReturn("SonarQube from NoWhere");
384 when(configuration.getPrefix()).thenReturn(SUBJECT_PREFIX);
385 when(server.getPublicRootUrl()).thenReturn("http://nemo.sonarsource.org");