/*
* Weblounge: Web Content Management System
* Copyright (c) 2003 - 2011 The Weblounge Team
* http://entwinemedia.com/weblounge
*
* This program 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 2
* of the License, or (at your option) any later version.
*
* This program 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 this program; if not, write to the Free Software Foundation
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package ch.entwine.weblounge.bridge.mail;
import ch.entwine.weblounge.common.content.ResourceURI;
import ch.entwine.weblounge.common.content.page.Page;
import ch.entwine.weblounge.common.content.page.PageTemplate;
import ch.entwine.weblounge.common.impl.content.page.PageImpl;
import ch.entwine.weblounge.common.impl.content.page.PageURIImpl;
import ch.entwine.weblounge.common.impl.content.page.PageletImpl;
import ch.entwine.weblounge.common.impl.security.UserImpl;
import ch.entwine.weblounge.common.language.Language;
import ch.entwine.weblounge.common.repository.WritableContentRepository;
import ch.entwine.weblounge.common.scheduler.JobException;
import ch.entwine.weblounge.common.scheduler.JobWorker;
import ch.entwine.weblounge.common.site.Site;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.util.Dictionary;
import java.util.Properties;
import java.util.UUID;
import javax.mail.Address;
import javax.mail.Flags.Flag;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.NoSuchProviderException;
import javax.mail.Part;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.internet.MimeBodyPart;
/**
* Content aggregator based on the <code>POP3</code> protocol.
*/
public class MailAggregator implements JobWorker {
/** Logging facility */
private static final Logger logger = LoggerFactory.getLogger(MailAggregator.class);
/** Name of the inbox */
public static final String INBOX = "INBOX";
/** Configuration key for the e-mail provider */
public static final String OPT_PROVIDER = "provider";
/** Default mail provider */
public static final String DEFAULT_PROVIDER = "pop3";
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.scheduler.JobWorker#execute(java.lang.String,
* java.util.Dictionary)
*/
public void execute(String name, Dictionary<String, Serializable> ctx)
throws JobException {
Site site = (Site) ctx.get(Site.class.getName());
// Make sure the site is ready to accept content
if (site.getContentRepository().isReadOnly()) {
logger.warn("Unable to publish e-mail messages to site '{}': repository is read only", site);
return;
}
WritableContentRepository repository = (WritableContentRepository)site.getContentRepository();
// Extract the configuration from the job properties
String provider = (String) ctx.get(OPT_PROVIDER);
Account account = null;
try {
if (StringUtils.isBlank(provider)) {
provider = DEFAULT_PROVIDER;
}
account = new Account(ctx);
} catch (IllegalArgumentException e) {
throw new JobException(this, e);
}
// Connect to the server
Properties sessionProperties = new Properties();
Session session = Session.getDefaultInstance(sessionProperties, null);
Store store = null;
Folder inbox = null;
try {
// Connect to the server
try {
store = session.getStore(provider);
store.connect(account.getHost(), account.getLogin(), account.getPassword());
} catch (NoSuchProviderException e) {
throw new JobException(this, "Unable to connect using unknown e-mail provider '" + provider + "'", e);
} catch (MessagingException e) {
throw new JobException(this, "Error connecting to " + provider + " account " + account, e);
}
// Open the account's inbox
try {
inbox = store.getFolder(INBOX);
if (inbox == null)
throw new JobException(this, "No inbox found at " + account);
inbox.open(Folder.READ_WRITE);
} catch (MessagingException e) {
throw new JobException(this, "Error connecting to inbox at " + account, e);
}
// Get the messages from the server
try {
for (Message message : inbox.getMessages()) {
if (!message.isSet(Flag.SEEN)) {
try {
Page page = aggregate(message, site);
message.setFlag(Flag.DELETED, true);
repository.put(page, true);
logger.info("E-Mail message published at {}", page.getURI());
} catch (Exception e) {
logger.info("E-Mail message discarded: {}", e.getMessage());
message.setFlag(Flag.SEEN, true);
// TODO: Reply to sender if the "from" field exists
}
}
}
} catch (MessagingException e) {
throw new JobException(this, "Error loading e-mail messages from inbox", e);
}
// Close the connection
// but don't remove the messages from the server
} finally {
if (inbox != null) {
try {
inbox.close(true);
} catch (MessagingException e) {
throw new JobException(this, "Error closing inbox", e);
}
}
if (store != null) {
try {
store.close();
} catch (MessagingException e) {
throw new JobException(this, "Error closing connection to e-mail server", e);
}
}
}
}
/**
* Aggregates the e-mail message by reading it and turning it either into a
* page or a file upload.
*
* @param message
* the e-mail message
* @param site
* the site to publish to
* @throws MessagingException
* if fetching the message data fails
* @throws IOException
* if writing the contents to the output stream fails
*/
protected Page aggregate(Message message, Site site) throws IOException,
MessagingException, IllegalArgumentException {
ResourceURI uri = new PageURIImpl(site, UUID.randomUUID().toString());
Page page = new PageImpl(uri);
Language language = site.getDefaultLanguage();
// Extract title and subject. Without these two, creating a page is not
// feasible, therefore both messages throw an IllegalArgumentException if
// the fields are not present.
String title = getSubject(message);
String author = getAuthor(message);
// Collect default settings
PageTemplate template = site.getDefaultTemplate();
if (template == null)
throw new IllegalStateException("Missing default template in site '" + site + "'");
String stage = template.getStage();
if (StringUtils.isBlank(stage))
throw new IllegalStateException("Missing stage definition in template '" + template.getIdentifier() + "'");
// Standard fields
page.setTitle(title, language);
page.setTemplate(template.getIdentifier());
page.setPublished(new UserImpl(site.getAdministrator()), message.getReceivedDate(), null);
// TODO: Translate e-mail "from" into site user and throw if no such
// user can be found
page.setCreated(site.getAdministrator(), message.getSentDate());
// Start looking at the message body
String contentType = message.getContentType();
if (StringUtils.isBlank(contentType))
throw new IllegalArgumentException("Message content type is unspecified");
// Text body
if (contentType.startsWith("text/plain")) {
// TODO: Evaluate charset
String body = null;
if (message.getContent() instanceof String)
body = (String)message.getContent();
else if (message.getContent() instanceof InputStream)
body = IOUtils.toString((InputStream)message.getContent());
else
throw new IllegalArgumentException("Message body is of unknown type");
return handleTextPlain(body, page, language);
}
// HTML body
if (contentType.startsWith("text/html")) {
// TODO: Evaluate charset
return handleTextHtml((String) message.getContent(), page, null);
}
// Multipart body
else if ("mime/multipart".equalsIgnoreCase(contentType)) {
Multipart mp = (Multipart) message.getContent();
for (int i = 0, n = mp.getCount(); i < n; i++) {
Part part = mp.getBodyPart(i);
String disposition = part.getDisposition();
if (disposition == null) {
MimeBodyPart mbp = (MimeBodyPart) part;
if (mbp.isMimeType("text/plain")) {
return handleTextPlain((String) mbp.getContent(), page, null);
} else {
// TODO: Implement special non-attachment cases here of
// image/gif, text/html, ...
throw new UnsupportedOperationException("Multipart message bodies of type '" + mbp.getContentType() + "' are not yet supported");
}
} else if (disposition.equals(Part.ATTACHMENT) || disposition.equals(Part.INLINE)) {
logger.info("Skipping message attachment " + part.getFileName());
// saveFile(part.getFileName(), part.getInputStream());
}
}
throw new IllegalArgumentException("Multipart message did not contain any recognizable content");
}
// ?
else {
throw new IllegalArgumentException("Message body is of unknown type '" + contentType + "'");
}
}
/**
* Handles the creation of a page based on an e-mail body of type
* <code>text/plain</code>.
*
* @param content
* the message body
* @param page
* the page
* @param language
* the content language
* @return the page
*/
private Page handleTextPlain(String content, Page page, Language language) {
for (String paragraph : content.split("\r\n")) {
if (StringUtils.isBlank(paragraph))
continue;
PageletImpl p = new PageletImpl("text", "paragraph");
p.setContent("text", StringUtils.trim(paragraph), language);
page.addPagelet(p, page.getStage().getIdentifier());
}
return page;
}
/**
* Handles the creation of a page based on an e-mail body of type
* <code>text/html</code>.
*
* @param content
* the message body
* @param page
* the page
* @param language
* the content language
* @return the page
*/
private Page handleTextHtml(String content, Page page, Language language) {
// TODO: Implement HTML message handling
throw new UnsupportedOperationException("Message bodies of type 'text/html' are not yet supported");
}
/**
* Returns the author of this message. If the message does not have a
* <code>from</code> field, an {@link IllegalArgumentException} is thrown.
*
* @param message
* the e-mail message
* @return the sender
* @throws MessagingException
* if reading the message's author fails
* @throws IllegalArgumentException
* if no author can be found
*/
private String getAuthor(Message message) throws MessagingException,
IllegalArgumentException {
Address[] address = message.getFrom();
if (address == null || address.length == 0)
throw new MessagingException("Message has no author");
return address[0].toString();
}
/**
* Returns the subject of this message. If the message does not have a
* <code>subject</code> field, an {@link IllegalArgumentException} is thrown.
*
* @param message
* the e-mail message
* @return the subject
* @throws MessagingException
* if reading the message's subject fails
* @throws IllegalArgumentException
* if no subject can be found
*/
private String getSubject(Message message) throws MessagingException,
IllegalArgumentException {
String subject = message.getSubject();
if (StringUtils.isBlank(subject))
throw new MessagingException("Message has no subject");
return subject;
}
}