/* * Copyright 2011 gitblit.com. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.gitblit.service; import java.io.File; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Properties; import java.util.Queue; import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.regex.Pattern; import javax.activation.DataHandler; import javax.activation.FileDataSource; import javax.mail.Authenticator; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.PasswordAuthentication; import javax.mail.Session; import javax.mail.Transport; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMultipart; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.gitblit.IStoredSettings; import com.gitblit.Keys; import com.gitblit.models.Mailing; import com.gitblit.utils.StringUtils; /** * The mail service handles sending email messages asynchronously from a queue. * * @author James Moger * */ public class MailService implements Runnable { private final Logger logger = LoggerFactory.getLogger(MailService.class); private final Queue queue = new ConcurrentLinkedQueue(); private final Session session; private final IStoredSettings settings; public MailService(IStoredSettings settings) { this.settings = settings; final String mailUser = settings.getString(Keys.mail.username, null); final String mailPassword = settings.getString(Keys.mail.password, null); final boolean smtps = settings.getBoolean(Keys.mail.smtps, false); boolean authenticate = !StringUtils.isEmpty(mailUser) && !StringUtils.isEmpty(mailPassword); String server = settings.getString(Keys.mail.server, ""); if (StringUtils.isEmpty(server)) { session = null; return; } int port = settings.getInteger(Keys.mail.port, 25); boolean isGMail = false; if (server.equals("smtp.gmail.com")) { port = 465; isGMail = true; } Properties props = new Properties(); props.setProperty("mail.smtp.host", server); props.setProperty("mail.smtp.port", String.valueOf(port)); props.setProperty("mail.smtp.auth", String.valueOf(authenticate)); props.setProperty("mail.smtp.auths", String.valueOf(authenticate)); if (isGMail || smtps) { props.setProperty("mail.smtp.starttls.enable", "true"); props.put("mail.smtp.socketFactory.port", String.valueOf(port)); props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); props.put("mail.smtp.socketFactory.fallback", "false"); } if (!StringUtils.isEmpty(mailUser) && !StringUtils.isEmpty(mailPassword)) { // SMTP requires authentication session = Session.getInstance(props, new Authenticator() { @Override protected PasswordAuthentication getPasswordAuthentication() { PasswordAuthentication passwordAuthentication = new PasswordAuthentication( mailUser, mailPassword); return passwordAuthentication; } }); } else { // SMTP does not require authentication session = Session.getInstance(props); } } /** * Indicates if the mail executor can send emails. * * @return true if the mail executor is ready to send emails */ public boolean isReady() { return session != null; } /** * Create a message. * * @param mailing * @return a message */ public Message createMessage(Mailing mailing) { if (mailing.subject == null) { mailing.subject = ""; } if (mailing.content == null) { mailing.content = ""; } Message message = new MailMessageImpl(session, mailing.id); try { String fromAddress = settings.getString(Keys.mail.fromAddress, null); if (StringUtils.isEmpty(fromAddress)) { fromAddress = "gitblit@gitblit.com"; } InternetAddress from = new InternetAddress(fromAddress, mailing.from == null ? "Gitblit" : mailing.from); message.setFrom(from); Pattern validEmail = Pattern .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})(\\]?)$"); // validate & add TO recipients List to = new ArrayList(); for (String address : mailing.toAddresses) { if (StringUtils.isEmpty(address)) { continue; } if (validEmail.matcher(address).find()) { try { to.add(new InternetAddress(address)); } catch (Throwable t) { } } } // validate & add CC recipients List cc = new ArrayList(); for (String address : mailing.ccAddresses) { if (StringUtils.isEmpty(address)) { continue; } if (validEmail.matcher(address).find()) { try { cc.add(new InternetAddress(address)); } catch (Throwable t) { } } } if (settings.getBoolean(Keys.web.showEmailAddresses, true)) { // full disclosure of recipients if (to.size() > 0) { message.setRecipients(Message.RecipientType.TO, to.toArray(new InternetAddress[to.size()])); } if (cc.size() > 0) { message.setRecipients(Message.RecipientType.CC, cc.toArray(new InternetAddress[cc.size()])); } } else { // everyone is bcc'd List bcc = new ArrayList(); bcc.addAll(to); bcc.addAll(cc); message.setRecipients(Message.RecipientType.BCC, bcc.toArray(new InternetAddress[bcc.size()])); } message.setSentDate(new Date()); message.setSubject(mailing.subject); MimeBodyPart messagePart = new MimeBodyPart(); messagePart.setText(mailing.content, "utf-8"); //messagePart.setHeader("Content-Transfer-Encoding", "quoted-printable"); if (Mailing.Type.html == mailing.type) { messagePart.setHeader("Content-Type", "text/html; charset=\"utf-8\""); } else { messagePart.setHeader("Content-Type", "text/plain; charset=\"utf-8\""); } MimeMultipart multiPart = new MimeMultipart(); multiPart.addBodyPart(messagePart); // handle attachments if (mailing.hasAttachments()) { for (File file : mailing.attachments) { if (file.exists()) { MimeBodyPart filePart = new MimeBodyPart(); FileDataSource fds = new FileDataSource(file); filePart.setDataHandler(new DataHandler(fds)); filePart.setFileName(fds.getName()); multiPart.addBodyPart(filePart); } } } message.setContent(multiPart); } catch (Exception e) { logger.error("Failed to properly create message", e); } return message; } /** * Returns the status of the mail queue. * * @return true, if the queue is empty */ public boolean hasEmptyQueue() { return queue.isEmpty(); } /** * Queue's an email message to be sent. * * @param message * @return true if the message was queued */ public boolean queue(Message message) { if (!isReady()) { return false; } try { message.saveChanges(); } catch (Throwable t) { logger.error("Failed to save changes to message!", t); } queue.add(message); return true; } @Override public void run() { if (!queue.isEmpty()) { if (session != null) { // send message via mail server List failures = new ArrayList(); Message message = null; while ((message = queue.poll()) != null) { try { if (settings.getBoolean(Keys.mail.debug, false)) { logger.info("send: " + StringUtils.trimString(message.getSubject(), 60)); } Transport.send(message); } catch (Throwable e) { logger.error("Failed to send message", e); failures.add(message); } } // push the failures back onto the queue for the next cycle queue.addAll(failures); } } } public void sendNow(Message message) throws Exception { Transport.send(message); } private static class MailMessageImpl extends MimeMessage { final String id; MailMessageImpl(Session session, String id) { super(session); this.id = id; } @Override protected void updateMessageID() throws MessagingException { if (!StringUtils.isEmpty(id)) { String hostname = "gitblit.com"; String refid = "<" + id + "@" + hostname + ">"; String mid = "<" + UUID.randomUUID().toString() + "@" + hostname + ">"; setHeader("References", refid); setHeader("In-Reply-To", refid); setHeader("Message-Id", mid); } } } }