James Moger
2015-09-30 7be48d8d14c74dfb10ba3d71f12a78505daf29a6
commit | author | age
831469 1 /*
JM 2  * Copyright 2011 gitblit.com.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
7bf6e1 16 package com.gitblit.service;
831469 17
d7c5a6 18 import java.io.File;
fa54be 19 import java.util.ArrayList;
831469 20 import java.util.Date;
JM 21 import java.util.List;
22 import java.util.Properties;
23 import java.util.Queue;
d7c5a6 24 import java.util.UUID;
831469 25 import java.util.concurrent.ConcurrentLinkedQueue;
fa54be 26 import java.util.regex.Pattern;
831469 27
d7c5a6 28 import javax.activation.DataHandler;
JM 29 import javax.activation.FileDataSource;
831469 30 import javax.mail.Authenticator;
JM 31 import javax.mail.Message;
d7c5a6 32 import javax.mail.MessagingException;
831469 33 import javax.mail.PasswordAuthentication;
JM 34 import javax.mail.Session;
35 import javax.mail.Transport;
36 import javax.mail.internet.InternetAddress;
d7c5a6 37 import javax.mail.internet.MimeBodyPart;
831469 38 import javax.mail.internet.MimeMessage;
d7c5a6 39 import javax.mail.internet.MimeMultipart;
2dfdd5 40 import javax.mail.internet.MimeUtility;
831469 41
JM 42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
7bf6e1 45 import com.gitblit.IStoredSettings;
JM 46 import com.gitblit.Keys;
d7c5a6 47 import com.gitblit.models.Mailing;
831469 48 import com.gitblit.utils.StringUtils;
JM 49
50 /**
7bf6e1 51  * The mail service handles sending email messages asynchronously from a queue.
699e71 52  *
831469 53  * @author James Moger
699e71 54  *
831469 55  */
7bf6e1 56 public class MailService implements Runnable {
831469 57
7bf6e1 58     private final Logger logger = LoggerFactory.getLogger(MailService.class);
831469 59
JM 60     private final Queue<Message> queue = new ConcurrentLinkedQueue<Message>();
61
62     private final Session session;
63
64     private final IStoredSettings settings;
65
7bf6e1 66     public MailService(IStoredSettings settings) {
831469 67         this.settings = settings;
JM 68
69         final String mailUser = settings.getString(Keys.mail.username, null);
70         final String mailPassword = settings.getString(Keys.mail.password, null);
14a770 71         final boolean smtps = settings.getBoolean(Keys.mail.smtps, false);
07ddbb 72         final boolean starttls = settings.getBoolean(Keys.mail.starttls, false);
831469 73         boolean authenticate = !StringUtils.isEmpty(mailUser) && !StringUtils.isEmpty(mailPassword);
JM 74         String server = settings.getString(Keys.mail.server, "");
75         if (StringUtils.isEmpty(server)) {
76             session = null;
77             return;
78         }
79         int port = settings.getInteger(Keys.mail.port, 25);
80         boolean isGMail = false;
81         if (server.equals("smtp.gmail.com")) {
82             port = 465;
83             isGMail = true;
84         }
85
86         Properties props = new Properties();
87         props.setProperty("mail.smtp.host", server);
88         props.setProperty("mail.smtp.port", String.valueOf(port));
89         props.setProperty("mail.smtp.auth", String.valueOf(authenticate));
90         props.setProperty("mail.smtp.auths", String.valueOf(authenticate));
07ddbb 91         props.setProperty("mail.smtp.starttls.enable", String.valueOf(starttls));
831469 92
14a770 93         if (isGMail || smtps) {
831469 94             props.setProperty("mail.smtp.starttls.enable", "true");
JM 95             props.put("mail.smtp.socketFactory.port", String.valueOf(port));
96             props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
97             props.put("mail.smtp.socketFactory.fallback", "false");
98         }
99
100         if (!StringUtils.isEmpty(mailUser) && !StringUtils.isEmpty(mailPassword)) {
101             // SMTP requires authentication
102             session = Session.getInstance(props, new Authenticator() {
699e71 103                 @Override
831469 104                 protected PasswordAuthentication getPasswordAuthentication() {
JM 105                     PasswordAuthentication passwordAuthentication = new PasswordAuthentication(
106                             mailUser, mailPassword);
107                     return passwordAuthentication;
108                 }
109             });
110         } else {
111             // SMTP does not require authentication
112             session = Session.getInstance(props);
113         }
114     }
115
116     /**
117      * Indicates if the mail executor can send emails.
699e71 118      *
831469 119      * @return true if the mail executor is ready to send emails
JM 120      */
121     public boolean isReady() {
122         return session != null;
123     }
124
125     /**
126      * Create a message.
699e71 127      *
d7c5a6 128      * @param mailing
831469 129      * @return a message
JM 130      */
d7c5a6 131     public Message createMessage(Mailing mailing) {
JM 132         if (mailing.subject == null) {
133             mailing.subject = "";
134         }
831469 135
d7c5a6 136         if (mailing.content == null) {
JM 137             mailing.content = "";
138         }
afaab5 139
d7c5a6 140         Message message = new MailMessageImpl(session, mailing.id);
831469 141         try {
7e099b 142             String fromAddress = settings.getString(Keys.mail.fromAddress, null);
JM 143             if (StringUtils.isEmpty(fromAddress)) {
144                 fromAddress = "gitblit@gitblit.com";
145             }
d7c5a6 146             InternetAddress from = new InternetAddress(fromAddress, mailing.from == null ? "Gitblit" : mailing.from);
831469 147             message.setFrom(from);
699e71 148
fa54be 149             Pattern validEmail = Pattern
JM 150                     .compile("^([a-zA-Z0-9_\\-\\.]+)@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.)|(([a-zA-Z0-9\\-]+\\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\\]?)$");
d7c5a6 151
JM 152             // validate & add TO recipients
153             List<InternetAddress> to = new ArrayList<InternetAddress>();
154             for (String address : mailing.toAddresses) {
fa54be 155                 if (StringUtils.isEmpty(address)) {
JM 156                     continue;
157                 }
158                 if (validEmail.matcher(address).find()) {
159                     try {
d7c5a6 160                         to.add(new InternetAddress(address));
fa54be 161                     } catch (Throwable t) {
JM 162                     }
163                 }
699e71 164             }
d7c5a6 165
JM 166             // validate & add CC recipients
167             List<InternetAddress> cc = new ArrayList<InternetAddress>();
168             for (String address : mailing.ccAddresses) {
169                 if (StringUtils.isEmpty(address)) {
170                     continue;
171                 }
172                 if (validEmail.matcher(address).find()) {
173                     try {
174                         cc.add(new InternetAddress(address));
175                     } catch (Throwable t) {
176                     }
177                 }
178             }
179
180             if (settings.getBoolean(Keys.web.showEmailAddresses, true)) {
181                 // full disclosure of recipients
182                 if (to.size() > 0) {
183                     message.setRecipients(Message.RecipientType.TO,
184                             to.toArray(new InternetAddress[to.size()]));
185                 }
186                 if (cc.size() > 0) {
187                     message.setRecipients(Message.RecipientType.CC,
188                             cc.toArray(new InternetAddress[cc.size()]));
189                 }
190             } else {
191                 // everyone is bcc'd
192                 List<InternetAddress> bcc = new ArrayList<InternetAddress>();
193                 bcc.addAll(to);
194                 bcc.addAll(cc);
195                 message.setRecipients(Message.RecipientType.BCC,
196                         bcc.toArray(new InternetAddress[bcc.size()]));
197             }
198
831469 199             message.setSentDate(new Date());
2dfdd5 200             // UTF-8 encode
W 201             message.setSubject(MimeUtility.encodeText(mailing.subject, "utf-8", "B"));
d7c5a6 202
JM 203             MimeBodyPart messagePart = new MimeBodyPart();
204             messagePart.setText(mailing.content, "utf-8");
205             //messagePart.setHeader("Content-Transfer-Encoding", "quoted-printable");
206
207             if (Mailing.Type.html == mailing.type) {
208                 messagePart.setHeader("Content-Type", "text/html; charset=\"utf-8\"");
209             } else {
210                 messagePart.setHeader("Content-Type", "text/plain; charset=\"utf-8\"");
211             }
212
213             MimeMultipart multiPart = new MimeMultipart();
214             multiPart.addBodyPart(messagePart);
215
216             // handle attachments
217             if (mailing.hasAttachments()) {
218                 for (File file : mailing.attachments) {
219                     if (file.exists()) {
220                         MimeBodyPart filePart = new MimeBodyPart();
221                         FileDataSource fds = new FileDataSource(file);
222                         filePart.setDataHandler(new DataHandler(fds));
223                         filePart.setFileName(fds.getName());
224                         multiPart.addBodyPart(filePart);
225                     }
226                 }
227             }
228
229             message.setContent(multiPart);
230
831469 231         } catch (Exception e) {
JM 232             logger.error("Failed to properly create message", e);
233         }
234         return message;
235     }
236
237     /**
7e099b 238      * Returns the status of the mail queue.
699e71 239      *
7e099b 240      * @return true, if the queue is empty
JM 241      */
242     public boolean hasEmptyQueue() {
243         return queue.isEmpty();
244     }
245
246     /**
831469 247      * Queue's an email message to be sent.
699e71 248      *
831469 249      * @param message
JM 250      * @return true if the message was queued
251      */
252     public boolean queue(Message message) {
253         if (!isReady()) {
254             return false;
255         }
256         try {
257             message.saveChanges();
258         } catch (Throwable t) {
259             logger.error("Failed to save changes to message!", t);
260         }
261         queue.add(message);
262         return true;
263     }
264
265     @Override
266     public void run() {
267         if (!queue.isEmpty()) {
268             if (session != null) {
269                 // send message via mail server
c6b6bd 270                 List<Message> failures = new ArrayList<Message>();
831469 271                 Message message = null;
c6b6bd 272                 while ((message = queue.poll()) != null) {
831469 273                     try {
JM 274                         if (settings.getBoolean(Keys.mail.debug, false)) {
c6b6bd 275                             logger.info("send: " + StringUtils.trimString(message.getSubject(), 60));
831469 276                         }
JM 277                         Transport.send(message);
278                     } catch (Throwable e) {
c6b6bd 279                         logger.error("Failed to send message", e);
JM 280                         failures.add(message);
831469 281                     }
JM 282                 }
699e71 283
c6b6bd 284                 // push the failures back onto the queue for the next cycle
JM 285                 queue.addAll(failures);
831469 286             }
JM 287         }
288     }
699e71 289
e8c417 290     public void sendNow(Message message) throws Exception {
JM 291         Transport.send(message);
292     }
d7c5a6 293
JM 294     private static class MailMessageImpl extends MimeMessage {
295
296         final String id;
297
298         MailMessageImpl(Session session, String id) {
299             super(session);
300             this.id = id;
301         }
302
303         @Override
304         protected void updateMessageID() throws MessagingException {
305             if (!StringUtils.isEmpty(id)) {
306                 String hostname = "gitblit.com";
307                 String refid = "<" + id + "@" + hostname + ">";
308                 String mid = "<" + UUID.randomUUID().toString() + "@" + hostname + ">";
309                 setHeader("References", refid);
310                 setHeader("In-Reply-To", refid);
311                 setHeader("Message-Id", mid);
312             }
313         }
314     }
831469 315 }