001    /*
002     * Sonar, open source software quality management tool.
003     * Copyright (C) 2008-2011 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    }