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}