/** * Copyright © 2002 Instituto Superior Técnico * * This file is part of FenixEdu Academic. * * FenixEdu Academic is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * FenixEdu Academic is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with FenixEdu Academic. If not, see <http://www.gnu.org/licenses/>. */ package org.fenixedu.academic.domain.util; import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Locale; import java.util.Map.Entry; import java.util.Properties; import javax.mail.Address; import javax.mail.BodyPart; import javax.mail.MessagingException; import javax.mail.SendFailedException; import javax.mail.Session; import javax.mail.Transport; import javax.mail.internet.AddressException; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMultipart; import javax.mail.internet.MimeUtility; import org.fenixedu.academic.FenixEduAcademicConfiguration; import org.fenixedu.academic.domain.Installation; import org.fenixedu.academic.domain.util.email.Message; import org.fenixedu.bennu.core.domain.Bennu; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import pt.ist.fenixframework.Atomic; import pt.ist.fenixframework.Atomic.TxMode; public class Email extends Email_Base { private static final Logger LOG = LoggerFactory.getLogger(Email.class); private static Session SESSION = null; private static int MAX_MAIL_RECIPIENTS; private static DateTimeFormatter rfc5322Fmt = DateTimeFormat.forPattern("EEE, dd MMM yyyy HH:mm:ss Z").withLocale( Locale.ENGLISH); private static synchronized Session init() { final Properties properties = new Properties(); properties.put("mail.smtp.host", FenixEduAcademicConfiguration.getConfiguration().getMailSmtpHost()); properties.put("mail.smtp.name", FenixEduAcademicConfiguration.getConfiguration().getMailSmtpName()); properties .put("mailSender.max.recipients", FenixEduAcademicConfiguration.getConfiguration().getMailSenderMaxRecipients()); properties.put("mail.debug", "false"); final Session tempSession = Session.getDefaultInstance(properties, null); MAX_MAIL_RECIPIENTS = Integer.parseInt(properties.getProperty("mailSender.max.recipients")); SESSION = tempSession; LOG.debug("Initialize mail properties"); for (Entry<Object, Object> entry : properties.entrySet()) { LOG.debug("\t{}={}", entry.getKey(), entry.getValue()); } return SESSION; } private Email() { super(); setRootDomainObjectFromEmailQueue(Bennu.getInstance()); } public Email(final String[] replyTos, final Collection<String> toAddresses, final Collection<String> ccAddresses, final Collection<String> bccAddresses, final Message message) { this(); setReplyTos(new EmailAddressList(replyTos == null ? null : Arrays.asList(replyTos))); setToAddresses(new EmailAddressList(toAddresses)); setCcAddresses(new EmailAddressList(ccAddresses)); setBccAddresses(new EmailAddressList(bccAddresses)); setMessage(message); } public String getBody() { return getMessage().getBody(); } public String getSubject() { return getMessage().getSubject(); } public String getHtmlBody() { return getMessage().getHtmlBody(); } public void delete() { setMessage(null); setRootDomainObjectFromEmailQueue(null); super.deleteDomainObject(); } public String[] replyTos() { return getReplyTos() == null ? null : getReplyTos().toArray(); } public Collection<String> toAddresses() { return getToAddresses() == null ? null : getToAddresses().toCollection(); } public Collection<String> ccAddresses() { return getCcAddresses() == null ? null : getCcAddresses().toCollection(); } public Collection<String> bccAddresses() { return getBccAddresses() == null ? null : getBccAddresses().toCollection(); } private void logProblem(final String description) { LOG.warn("Sending of email {} failed. Description: {}", this.getExternalId(), description); } private void logProblem(final MessagingException e) { logProblem(e.getMessage()); final Exception nextException = e.getNextException(); if (nextException != null) { if (nextException instanceof MessagingException) { logProblem((MessagingException) nextException); } else { logProblem(nextException.getMessage()); } } } private void abort() { final Collection<String> failed = new HashSet<String>(); final EmailAddressList failedAddresses = getFailedAddresses(); if (failedAddresses != null && !failedAddresses.isEmpty()) { failed.addAll(failedAddresses.toCollection()); } final EmailAddressList toAddresses = getToAddresses(); if (toAddresses != null && !toAddresses.isEmpty()) { failed.addAll(toAddresses.toCollection()); } final EmailAddressList ccAddresses = getCcAddresses(); if (ccAddresses != null && !ccAddresses.isEmpty()) { failed.addAll(ccAddresses.toCollection()); } final EmailAddressList bccAddresses = getBccAddresses(); if (bccAddresses != null && !bccAddresses.isEmpty()) { failed.addAll(bccAddresses.toCollection()); } final EmailAddressList emailAddressList = new EmailAddressList(failed); setFailedAddresses(emailAddressList); setToAddresses(null); setCcAddresses(null); setBccAddresses(null); } private void retry(final EmailAddressList toAddresses, final EmailAddressList ccAddresses, final EmailAddressList bccAddresses) { setToAddresses(toAddresses); setCcAddresses(ccAddresses); setBccAddresses(bccAddresses); } private static String encode(final String string) { try { return string == null ? "" : MimeUtility.encodeText(string); } catch (final UnsupportedEncodingException e) { LOG.error(e.getMessage(), e); return string; } } protected static String constructFromString(final String fromName, String fromAddress) { return (fromName == null || fromName.length() == 0) ? fromAddress : fromName.replace(',', ' ') + " <" + fromAddress + ">"; } private class EmailMimeMessage extends MimeMessage { private String fenixMessageId = null; public EmailMimeMessage() { super(SESSION == null ? init() : SESSION); } @Override public String getMessageID() throws MessagingException { if (fenixMessageId == null) { final String externalId = getExternalId(); final String instituitionEmailDomain = Installation.getInstance().getInstituitionEmailDomain(); fenixMessageId = "<" + externalId + "." + new DateTime().getMillis() + "@" + instituitionEmailDomain + ">"; } return fenixMessageId; } @Override protected void updateMessageID() throws MessagingException { setHeader("Message-ID", getMessageID()); setHeader("Date", rfc5322Fmt.print(getMessage().getCreated())); } public void send(final Email email) throws MessagingException { if (email.getMessage().getSender().getFromName() == null) { logProblem("error.from.address.cannot.be.null"); abort(); return; } final String from = constructFromString(encode(email.getMessage().getFromName()), email.getMessage().getFromAddress()); final String[] replyTos = email.replyTos(); final Address[] replyToAddresses = new Address[replyTos == null ? 0 : replyTos.length]; if (replyTos != null) { for (int i = 0; i < replyTos.length; i++) { try { replyToAddresses[i] = new InternetAddress(encode(replyTos[i])); } catch (final AddressException e) { logProblem("invalid.reply.to.address: " + replyTos[i]); abort(); return; } } } setFrom(new InternetAddress(from)); setSubject(encode(email.getSubject())); setReplyTo(replyToAddresses); final String body = email.getBody(); final String htmlBody = getHtmlBody(); final MimeMultipart mimeMultipart = createMimeMultipart(body, htmlBody); if (body != null && !body.trim().isEmpty()) { final BodyPart bodyPart = new MimeBodyPart(); bodyPart.setText(body); mimeMultipart.addBodyPart(bodyPart); } if (htmlBody != null && !htmlBody.trim().isEmpty()) { final BodyPart bodyPart = new MimeBodyPart(); bodyPart.setContent(htmlBody, "text/html; charset=utf-8"); mimeMultipart.addBodyPart(bodyPart); } setContent(mimeMultipart); addRecipientsAux(); LOG.info("Sending email {} with message id {}", email.getExternalId(), getMessageID()); Transport.send(this); final Address[] allRecipients = getAllRecipients(); setConfirmedAddresses(allRecipients); } private MimeMultipart createMimeMultipart(final String body, final String htmlBody) { return body != null && !body.trim().isEmpty() && htmlBody != null && !htmlBody.trim().isEmpty() ? new MimeMultipart( "alternative") : new MimeMultipart(); } private void addRecipientsAux() { boolean hasAnyToOrCC = false; if (hasAnyRecipients(getToAddresses())) { final EmailAddressList tos = getToAddresses(); final EmailAddressList remainder = addRecipientsAux(RecipientType.TO, tos); setToAddresses(remainder); hasAnyToOrCC = true; } if (hasAnyRecipients(getCcAddresses())) { final EmailAddressList ccs = getCcAddresses(); final EmailAddressList remainder = addRecipientsAux(RecipientType.CC, ccs); setCcAddresses(remainder); hasAnyToOrCC = true; } if (!hasAnyToOrCC && hasAnyRecipients(getBccAddresses())) { final EmailAddressList bccs = getBccAddresses(); final EmailAddressList remainder = addRecipientsAux(RecipientType.BCC, bccs); setBccAddresses(remainder); } } private EmailAddressList addRecipientsAux(final javax.mail.Message.RecipientType recipientType, final EmailAddressList emailAddressList) { final String[] emailAddresses = emailAddressList.toArray(); for (int i = 0; i < emailAddresses.length; i++) { final String emailAddress = emailAddresses[i]; try { if (emailAddressFormatIsValid(emailAddress)) { addRecipient(recipientType, new InternetAddress(encode(emailAddress))); } else { logProblem("invalid.email.address.format: " + emailAddress); } } catch (final AddressException e) { logProblem(e.getMessage() + " " + emailAddress); } catch (final MessagingException e) { logProblem(e.getMessage() + " " + emailAddress); } if (i == MAX_MAIL_RECIPIENTS && i + 1 < emailAddresses.length) { final String all = emailAddressList.toString(); final int next = all.indexOf(emailAddress) + emailAddress.length() + 2; return new EmailAddressList(all.substring(next)); } } return null; } public boolean emailAddressFormatIsValid(String emailAddress) { if ((emailAddress == null) || (emailAddress.length() == 0)) { return false; } if (emailAddress.indexOf(' ') > 0) { return false; } String[] atSplit = emailAddress.split("@"); if (atSplit.length != 2) { return false; } else if ((atSplit[0].length() == 0) || (atSplit[1].length() == 0)) { return false; } String domain = new String(atSplit[1]); if (domain.lastIndexOf('.') == (domain.length() - 1)) { return false; } if (domain.indexOf('.') <= 0) { return false; } return true; } } private void setConfirmedAddresses(final Address[] recipients) { final Collection<String> addresses = new HashSet<String>(); final EmailAddressList confirmedAddresses = getConfirmedAddresses(); if (confirmedAddresses != null && !confirmedAddresses.isEmpty()) { addresses.addAll(confirmedAddresses.toCollection()); } if (recipients != null) { for (final Address address : recipients) { addresses.add(address.toString()); } } setConfirmedAddresses(new EmailAddressList(addresses)); } private void setFailedAddresses(final Address[] recipients) { final Collection<String> addresses = new HashSet<String>(); final EmailAddressList failedAddresses = getFailedAddresses(); if (failedAddresses != null && !failedAddresses.isEmpty()) { addresses.addAll(failedAddresses.toCollection()); } if (recipients != null) { for (final Address address : recipients) { addresses.add(address.toString()); } } setFailedAddresses(new EmailAddressList(addresses)); } private void resend(final Address[] recipients) { // final Collection<String> addresses = new HashSet<String>(); // final EmailAddressList bccAddresses = getBccAddresses(); // if (bccAddresses != null && !bccAddresses.isEmpty()) { // addresses.addAll(bccAddresses.toCollection()); // } if (recipients != null && recipients.length > 0) { for (final Address address : recipients) { final String[] replyTos = getReplyTos() == null ? null : getReplyTos().toArray(); new Email(replyTos, Collections.emptySet(), Collections.emptySet(), Collections.singleton(address.toString()), getMessage()); // addresses.add(address.toString()); } } // setBccAddresses(new EmailAddressList(addresses)); } @Atomic(mode = TxMode.WRITE) public void deliver() { if (!hasAnyRecipients() || (getMessage() != null && getMessage().getCreated().plusDays(5).isBeforeNow())) { setRootDomainObjectFromEmailQueue(null); } else { final EmailAddressList toAddresses = getToAddresses(); final EmailAddressList ccAddresses = getCcAddresses(); final EmailAddressList bccAddresses = getBccAddresses(); final EmailMimeMessage emailMimeMessage = new EmailMimeMessage(); try { emailMimeMessage.send(this); } catch (final SendFailedException e) { logProblem(e); final Address[] invalidAddresses = e.getInvalidAddresses(); setFailedAddresses(invalidAddresses); final Address[] validSentAddresses = e.getValidSentAddresses(); setConfirmedAddresses(validSentAddresses); final Address[] validUnsentAddresses = e.getValidUnsentAddresses(); resend(validUnsentAddresses); } catch (final MessagingException e) { logProblem(e); // abort(); retry(toAddresses, ccAddresses, bccAddresses); } if (!hasAnyRecipients()) { setRootDomainObjectFromEmailQueue(null); } } } private boolean hasAnyRecipients() { return hasAnyRecipients(getToAddresses()) || hasAnyRecipients(getCcAddresses()) || hasAnyRecipients(getBccAddresses()); } private boolean hasAnyRecipients(final EmailAddressList emailAddressList) { return emailAddressList != null && !emailAddressList.isEmpty(); } }