/**********************************************************************************
* $URL: https://source.sakaiproject.org/svn/mailarchive/trunk/mailarchive-james/james/src/java/org/sakaiproject/james/SakaiMailet.java $
* $Id: SakaiMailet.java 105079 2012-02-24 23:08:11Z ottenhoff@longsight.com $
***********************************************************************************
*
* Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 The Sakai Foundation
*
* Licensed under the Educational Community 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.opensource.org/licenses/ECL-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 org.sakaiproject.james;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
import javax.mail.Address;
import javax.mail.BodyPart;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Part;
import javax.mail.internet.ContentType;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeUtility;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.mailet.GenericMailet;
import org.apache.mailet.Mail;
import org.apache.mailet.MailAddress;
import org.sakaiproject.alias.api.AliasService;
import org.sakaiproject.component.api.ServerConfigurationService;
import org.sakaiproject.component.cover.ComponentManager;
import org.sakaiproject.content.api.ContentHostingService;
import org.sakaiproject.content.api.ContentResource;
import org.sakaiproject.entity.api.EntityManager;
import org.sakaiproject.entity.api.Reference;
import org.sakaiproject.entity.api.ResourceProperties;
import org.sakaiproject.entity.api.ResourcePropertiesEdit;
import org.sakaiproject.exception.IdUnusedException;
import org.sakaiproject.exception.PermissionException;
import org.sakaiproject.mailarchive.api.MailArchiveChannel;
import org.sakaiproject.mailarchive.cover.MailArchiveService;
import org.sakaiproject.site.api.SiteService;
import org.sakaiproject.thread_local.api.ThreadLocalManager;
import org.sakaiproject.time.api.TimeService;
import org.sakaiproject.tool.api.Session;
import org.sakaiproject.tool.api.SessionManager;
import org.sakaiproject.user.api.User;
import org.sakaiproject.user.api.UserDirectoryService;
import org.sakaiproject.user.api.UserNotDefinedException;
import org.sakaiproject.util.ResourceLoader;
import org.sakaiproject.util.Validator;
import org.sakaiproject.util.Web;
/**
* <p>
* SakaiMailet watches incoming mail (via James) and sends mail to the appropriate mail archive channel in Sakai.
* </p>
*/
public class SakaiMailet extends GenericMailet
{
/** Resource bundle using current language locale */
private static ResourceLoader rb = new ResourceLoader("sakaimailet");
/** Our logger. */
private static Log M_log = LogFactory.getLog(SakaiMailet.class);
/** The user name of the postmaster user - the one who posts incoming mail. */
public static final String POSTMASTER = "postmaster";
// used when parsing email header parts
private static final String NAME_PREFIX = "name=";
private static String PROCESSOR_ROOT = "root";
private static String PROCESSOR_ERROR = Mail.ERROR;
private static String PROCESSOR_TRANSPORT = Mail.TRANSPORT;
/*
* root processor is first (redirects to transport mostly), then error, then transport (which is where the SakaiMailet is engaged)
* then the processors in the order listed below
*
* From: http://james.apache.org/server/2.3.0/spoolmanager_configuration.html
* The James SpoolManager creates a correspondence between processor names and the "state" of a mail as defined in the Mailet API.
* Specifically, after each mailet processes a mail, the state of the message is examined.
* If the state has been changed, the message does not continue in the current processor.
* If the new state is "ghost" then processing of that message terminates completely.
* If the new state is anything else, the message is re-routed to the processor with the name matching the new state.
*/
private static String PROCESSOR_SPAM = "spam";
private static String PROCESSOR_VIRUS = "virus";
private static String PROCESSOR_LOCAL_ADDRESS_ERROR = "local-address-error";
private static String PROCESSOR_RELAY_DENIED = "relay-denied";
private static String PROCESSOR_BOUNCES = "bounces";
// indicates no more processing should occur
private static String PROCESSOR_GHOST = Mail.GHOST;
/**
* The james processor order - NOTE: this MUST match with the config in:
* /sakai-mailarchive-james/src/webapp/apps/james/SAR-INF/config.xml
*/
private static String[] PROCESSORS = {
PROCESSOR_ROOT,
PROCESSOR_ERROR,
PROCESSOR_TRANSPORT,
PROCESSOR_SPAM,
PROCESSOR_VIRUS,
PROCESSOR_LOCAL_ADDRESS_ERROR,
PROCESSOR_RELAY_DENIED,
PROCESSOR_BOUNCES,
PROCESSOR_GHOST
};
private AliasService aliasService = null;
private ContentHostingService contentHostingService = null;
private EntityManager entityManager = null;
private SiteService siteService = null;
private ThreadLocalManager threadLocalManager = null;
private TimeService timeService = null;
private SessionManager sessionManager = null;
private UserDirectoryService userDirectoryService = null;
private ServerConfigurationService serverConfigurationService = null;
private String courseMailArchiveDisabledProcessor = null;
private String courseMailArchiveNotExistsProcessor = null;
private String userNotAllowedToPostProcessor = null;
/**
* Called when created.
*/
public void init() throws MessagingException
{
M_log.info("init()");
// load the services
aliasService = requireService(AliasService.class);
contentHostingService = requireService(ContentHostingService.class);
entityManager = requireService(EntityManager.class);
siteService = requireService(SiteService.class);
threadLocalManager = requireService(ThreadLocalManager.class);
timeService = requireService(TimeService.class);
sessionManager = requireService(SessionManager.class);
userDirectoryService = requireService(UserDirectoryService.class);
serverConfigurationService = requireService(ServerConfigurationService.class);
// load up the configuration options for the sakai mailet processor
courseMailArchiveDisabledProcessor = serverConfigurationService.getString("smtp.archive.disabled.processor", PROCESSOR_GHOST);
if (courseMailArchiveDisabledProcessor == null
|| "none".equalsIgnoreCase(courseMailArchiveDisabledProcessor)
|| !Arrays.asList(PROCESSORS).contains(courseMailArchiveDisabledProcessor)) {
courseMailArchiveDisabledProcessor = PROCESSOR_GHOST; // DEFAULT
}
courseMailArchiveNotExistsProcessor = serverConfigurationService.getString("smtp.archive.address.invalid.processor", PROCESSOR_GHOST);
if (courseMailArchiveNotExistsProcessor == null
|| "none".equalsIgnoreCase(courseMailArchiveNotExistsProcessor)
|| !Arrays.asList(PROCESSORS).contains(courseMailArchiveNotExistsProcessor)) {
courseMailArchiveNotExistsProcessor = PROCESSOR_GHOST; // DEFAULT
}
userNotAllowedToPostProcessor = serverConfigurationService.getString("smtp.user.not.allowed.processor", PROCESSOR_GHOST);
if (userNotAllowedToPostProcessor == null
|| "none".equalsIgnoreCase(userNotAllowedToPostProcessor)
|| !Arrays.asList(PROCESSORS).contains(userNotAllowedToPostProcessor)) {
userNotAllowedToPostProcessor = PROCESSOR_GHOST; // DEFAULT
}
M_log.info("MailArchiveDisabledProcessor="+courseMailArchiveDisabledProcessor+", MailArchiveNotExistsProcessor="+courseMailArchiveNotExistsProcessor+", UserNotAllowedToPostProcessor"+userNotAllowedToPostProcessor);
}
/**
* Get the service for a class or die
* @param serviceClass
* @return the service class
* @throws IllegalStateException if the service cannot be found
*/
@SuppressWarnings({"unchecked"})
private <T> T requireService(Class<T> serviceClass) {
Object service = ComponentManager.get(serviceClass);
if (service == null) {
throw new IllegalStateException("Unable to get service ("+serviceClass+") from Sakai ComponentManager");
}
return (T) service;
}
/**
* Called when leaving.
*/
public void destroy()
{
M_log.info("destroy()");
super.destroy();
} // destroy
/**
* Process incoming mail.
*
* @param mail
* ...
*/
public void service(Mail mail) throws MessagingException
{
// get the postmaster user
User postmaster = null;
try
{
postmaster = userDirectoryService.getUser(POSTMASTER);
}
catch (UserNotDefinedException e)
{
M_log.warn("service(): no postmaster, incoming mail will not be processed until a postmaster user (id="+POSTMASTER+") exists in this Sakai instance");
mail.setState(Mail.GHOST);
return;
}
try
{
// set the current user to postmaster
Session s = sessionManager.getCurrentSession();
if (s != null)
{
s.setUserId(postmaster.getId());
}
else
{
M_log.warn("service - no SessionManager.getCurrentSession, cannot set to postmaser user, attempting to use the current user ("
+sessionManager.getCurrentSessionUserId()+") and session ("+sessionManager.getCurrentSession().getId()+")");
}
MimeMessage msg = mail.getMessage();
String id = msg.getMessageID();
Address[] fromAddresses = msg.getFrom();
String from = null;
String fromAddr = null;
if ((fromAddresses != null) && (fromAddresses.length == 1))
{
from = fromAddresses[0].toString();
if (fromAddresses[0] instanceof InternetAddress)
{
fromAddr = ((InternetAddress) (fromAddresses[0])).getAddress();
}
}
else
{
from = mail.getSender().toString();
fromAddr = mail.getSender().toInternetAddress().getAddress();
}
Collection<MailAddress> to = mail.getRecipients();
Date sent = msg.getSentDate();
String subject = StringUtils.trimToNull(msg.getSubject());
Enumeration<String> headers = msg.getAllHeaderLines();
List<String> mailHeaders = new Vector<String>();
while (headers.hasMoreElements())
{
String line = (String) headers.nextElement();
// check if string starts with "Content-Type", ignoring case
if (line.regionMatches(true, 0, MailArchiveService.HEADER_CONTENT_TYPE, 0, MailArchiveService.HEADER_CONTENT_TYPE.length()))
{
String contentType = line.substring(0, MailArchiveService.HEADER_CONTENT_TYPE.length() );
mailHeaders.add(line.replaceAll(contentType, MailArchiveService.HEADER_OUTER_CONTENT_TYPE));
}
// don't copy null subject lines. we'll add a real one below
if (!(line.regionMatches(true, 0, MailArchiveService.HEADER_SUBJECT, 0, MailArchiveService.HEADER_SUBJECT.length()) &&
subject == null))
mailHeaders.add(line);
}
//Add headers for a null subject, keep null in DB
if (subject == null) {
mailHeaders.add(MailArchiveService.HEADER_SUBJECT + ": <"+ rb.getString("err_no_subject") +">");
}
if (M_log.isDebugEnabled()) {
M_log.debug(id + " : mail: from:" + from + " sent: " + timeService.newTime(sent.getTime()).toStringLocalFull()
+ " subject: " + subject);
}
// process for each recipient
Iterator<MailAddress> it = to.iterator();
while (it.hasNext())
{
String mailId = null;
try
{
MailAddress recipient = (MailAddress) it.next();
if (M_log.isDebugEnabled()) {
M_log.debug(id + " : checking to: " + recipient);
}
// the recipient's mail id
mailId = recipient.getUser();
// eat the no-reply
if ("no-reply".equalsIgnoreCase(mailId))
{
mail.setState(Mail.GHOST);
if (M_log.isInfoEnabled()) {
M_log.info("Incoming message mailId ("+mailId+") set to no-reply, mail processing cancelled");
}
/* NOTE: this doesn't make a lot of sense to me, once the mail is ghosted
* then it won't be processed anymore so continuing is kind of a waste of time,
* shouldn't this just break instead?
*/
continue;
}
// find the channel (mailbox) that this is addressed to
// for now, check only for it being a site or alias to a site.
// %%% - add user and other later -ggolden
MailArchiveChannel channel = null;
// first, assume the mailId is a site id
String channelRef = MailArchiveService.channelReference(mailId, SiteService.MAIN_CONTAINER);
try
{
channel = MailArchiveService.getMailArchiveChannel(channelRef);
if (M_log.isDebugEnabled()) {
M_log.debug("Incoming message mailId ("+mailId+") IS a valid site channel reference");
}
}
catch (IdUnusedException goOn) {
// INDICATES the incoming message is NOT for a currently valid site
if (M_log.isDebugEnabled()) {
M_log.debug("Incoming message mailId ("+mailId+") is NOT a valid site channel reference, will attempt more matches");
}
} catch (PermissionException e) {
// INDICATES the channel is valid but the user has no permission to access it
// This generally should not happen because the current user should be the postmaster
M_log.warn("mailarchive failure: message processing cancelled: PermissionException with channelRef ("+channelRef
+") - user not allowed to get this mail archive channel: (id="+id+") (mailId="+mailId+") (user="
+sessionManager.getCurrentSessionUserId()+") (session="+sessionManager.getCurrentSession().getId()+"): " + e, e);
// BOUNCE REPLY - send a message back to the user to let them know their email failed
String errMsg = rb.getString("err_not_member") + "\n\n";
String mailSupport = StringUtils.trimToNull( serverConfigurationService.getString("mail.support") );
if ( mailSupport != null ) {
errMsg +=(String) rb.getFormattedMessage("err_questions", new Object[]{mailSupport})+"\n";
}
mail.setErrorMessage(errMsg);
mail.setState(userNotAllowedToPostProcessor);
continue;
}
// next, if not a site, see if it's an alias to a site or channel
if (channel == null) {
// if not an alias, it will throw the IdUnusedException caught below
Reference ref = entityManager.newReference(aliasService.getTarget(mailId));
if (ref.getType().equals(SiteService.APPLICATION_ID)) {
// ref is a site
// now we have a site reference, try for it's channel
channelRef = MailArchiveService.channelReference(ref.getId(), SiteService.MAIN_CONTAINER);
if (M_log.isDebugEnabled()) {
M_log.debug("Incoming message mailId ("+mailId+") IS a valid site reference ("+ref.getId()+")");
}
} else if (ref.getType().equals(MailArchiveService.APPLICATION_ID)) {
// ref is a channel
channelRef = ref.getReference();
if (M_log.isDebugEnabled()) {
M_log.debug("Incoming message mailId ("+mailId+") IS a valid channel reference ("+ref.getId()+")");
}
} else {
// ref cannot be be matched
if (M_log.isInfoEnabled()) {
M_log.info(id + " : mail rejected: unknown address: " + mailId + " : mailId ("+mailId+") does NOT match site, alias, or other current channel");
}
if (M_log.isDebugEnabled()) {
M_log.debug("Incoming message mailId ("+mailId+") is NOT a valid does NOT match site, alias, or other current channel reference ("+ref.getId()+"), message rejected");
}
throw new IdUnusedException(mailId);
}
// if there's no channel for this site, it will throw the IdUnusedException caught below
try {
channel = MailArchiveService.getMailArchiveChannel(channelRef);
} catch (PermissionException e) {
// INDICATES the channel is valid but the user has no permission to access it
// This generally should not happen because the current user should be the postmaster
M_log.warn("mailarchive failure: message processing cancelled: PermissionException with channelRef ("+channelRef
+") - user not allowed to get this mail archive channel: (id="+id+") (mailId="+mailId+") (user="
+sessionManager.getCurrentSessionUserId()+") (session="+sessionManager.getCurrentSession().getId()+"): " + e, e);
// BOUNCE REPLY - send a message back to the user to let them know their email failed
String errMsg = rb.getString("err_not_member") + "\n\n";
String mailSupport = StringUtils.trimToNull( serverConfigurationService.getString("mail.support") );
if ( mailSupport != null ) {
errMsg +=(String) rb.getFormattedMessage("err_questions", new Object[]{mailSupport})+"\n";
}
mail.setErrorMessage(errMsg);
mail.setState(userNotAllowedToPostProcessor);
continue;
}
if (M_log.isDebugEnabled()) {
M_log.debug("Incoming message mailId ("+mailId+") IS a valid channel ("+channelRef+"), found channel: "+channel);
}
}
if (channel == null) {
if (M_log.isDebugEnabled()) {
M_log.debug("Incoming message mailId ("+mailId+"), channelRef ("+channelRef+") could not be resolved and is null: "+channel);
}
// this should never happen but it is here just in case
throw new IdUnusedException(mailId);
}
// skip disabled channels
if (!channel.getEnabled())
{
// INDICATES that the channel is NOT currently enabled so no messages can be received
if (from.startsWith(POSTMASTER)) {
mail.setState(Mail.GHOST);
} else {
// BOUNCE REPLY - send a message back to the user to let them know their email failed
String errMsg = rb.getString("err_email_off") + "\n\n";
String mailSupport = StringUtils.trimToNull( serverConfigurationService.getString("mail.support") );
if ( mailSupport != null ) {
errMsg +=(String) rb.getFormattedMessage("err_questions", new Object[]{mailSupport})+"\n";
}
mail.setErrorMessage(errMsg);
mail.setState(courseMailArchiveDisabledProcessor);
}
if (M_log.isInfoEnabled()) {
M_log.info(id + " : mail rejected: channel ("+channelRef+") not enabled: " + mailId);
}
continue;
}
// for non-open channels, make sure the from is a member
if (!channel.getOpen())
{
// see if our fromAddr is the email address of any of the users who are permitted to add messages to the channel.
if (!fromValidUser(fromAddr, channel))
{
// INDICATES user is not allowed to send messages to this group
if (M_log.isInfoEnabled()) {
M_log.info(id + " : mail rejected: from: " + fromAddr + " not authorized for site: " + mailId + " and channel ("+channelRef+")");
}
// BOUNCE REPLY - send a message back to the user to let them know their email failed
String errMsg = rb.getString("err_not_member") + "\n\n";
String mailSupport = StringUtils.trimToNull( serverConfigurationService.getString("mail.support") );
if ( mailSupport != null ) {
errMsg +=(String) rb.getFormattedMessage("err_questions", new Object[]{mailSupport})+"\n";
}
mail.setErrorMessage(errMsg);
mail.setState(userNotAllowedToPostProcessor);
continue;
}
}
// prepare the message
StringBuilder bodyBuf[] = new StringBuilder[2];
bodyBuf[0] = new StringBuilder();
bodyBuf[1] = new StringBuilder();
List<Reference> attachments = entityManager.newReferenceList();
String siteId = null;
if (siteService.siteExists(channel.getContext())) {
siteId = channel.getContext();
}
try
{
StringBuilder bodyContentType = new StringBuilder();
parseParts(siteId, msg, id, bodyBuf, bodyContentType, attachments, Integer.valueOf(-1));
if (bodyContentType.length() > 0)
{
// save the content type of the message body - which may be different from the
// overall MIME type of the message (multipart, etc)
mailHeaders.add(MailArchiveService.HEADER_INNER_CONTENT_TYPE + ": " + bodyContentType);
}
}
catch (MessagingException e)
{
// NOTE: if this happens it just means we don't get the extra header, not the end of the world
//e.printStackTrace();
M_log.warn("MessagingException: service(): msg.getContent() threw: " + e, e);
}
catch (IOException e)
{
// NOTE: if this happens it just means we don't get the extra header, not the end of the world
//e.printStackTrace();
M_log.warn("IOException: service(): msg.getContent() threw: " + e, e);
}
mailHeaders.add("List-Id: <"+ channel.getId()+ ".localhost>");
// post the message to the group's channel
String body[] = new String[2];
body[0] = bodyBuf[0].toString(); // plain/text
body[1] = bodyBuf[1].toString(); // html/text
try {
if (channel.getReplyToList())
{
List<String> modifiedHeaders = new Vector<String>();
for (String header: (List<String>)mailHeaders)
{
if (header != null && !header.startsWith("Reply-To:"))
{
modifiedHeaders.add(header);
}
}
// Note: can't use recipient, since it's host may be configured as mailId@myhost.james
String mailHost = serverConfigurationService.getServerName();
if ( mailHost == null || mailHost.trim().equals("") )
mailHost = mail.getRemoteHost();
MailAddress replyTo = new MailAddress( mailId, mailHost );
if (M_log.isDebugEnabled()) {
M_log.debug("Set Reply-To address to "+ replyTo.toString());
}
modifiedHeaders.add("Reply-To: "+ replyTo.toString());
// post the message to the group's channel
channel.addMailArchiveMessage(subject, from.toString(), timeService.newTime(sent.getTime()), modifiedHeaders,
attachments, body);
}
else
{
// post the message to the group's channel
channel.addMailArchiveMessage(subject, from.toString(), timeService.newTime(sent.getTime()), mailHeaders,
attachments, body);
}
} catch (PermissionException pe) {
// INDICATES that the current user does not have permission to add or get the mail archive message from the current channel
// This generally should not happen because the current user should be the postmaster
M_log.warn("mailarchive PermissionException message service failure: (id="+id+") (mailId="+mailId+") : " + pe, pe);
mail.setState(Mail.GHOST); // ghost out the message because this should not happen
}
if (M_log.isDebugEnabled()) {
M_log.debug(id + " : delivered to:" + mailId);
}
// all is happy - ghost the message to stop further processing
mail.setState(Mail.GHOST);
}
catch (IdUnusedException goOn)
{
// INDICATES that the channelReference found above was actually invalid OR that no channel reference could be identified
// if this is to the postmaster, and there's no site, channel or alias for the postmaster, then quietly eat the message
if (POSTMASTER.equals(mailId) || from.startsWith(POSTMASTER + "@"))
{
mail.setState(Mail.GHOST);
continue;
}
// BOUNCE REPLY - send a message back to the user to let them know their email failed
if (M_log.isInfoEnabled()) {
M_log.info("mailarchive invalid or unusable channel reference ("+mailId+"): "+id + " : mail rejected: " + goOn.toString());
}
String errMsg = rb.getString("err_addr_unknown") + "\n\n";
String mailSupport = StringUtils.trimToNull( serverConfigurationService.getString("mail.support") );
if ( mailSupport != null ) {
errMsg +=(String) rb.getFormattedMessage("err_questions", new Object[]{mailSupport})+"\n";
}
mail.setErrorMessage(errMsg);
mail.setState(courseMailArchiveNotExistsProcessor);
}
catch (Exception ex)
{
// INDICATES that some general exception has occurred which we did not expect
// This definitely should NOT happen
M_log.error("mailarchive General message service exception: (id="+id+") (mailId="+mailId+") : " + ex, ex);
mail.setState(Mail.GHOST); // ghost the message to stop it from being further processed
}
}
}
finally
{
// clear out any current current bindings
threadLocalManager.clear();
}
}
/**
* Check if the fromAddr email address is recognized as belonging to a user who has permission to add to the channel.
*
* @param fromAddr
* The email address to check.
* @param channel
* The mail archive channel.
* @return True if the email address is from a user who is authorized to add mail, false if not.
*/
protected boolean fromValidUser(String fromAddr, MailArchiveChannel channel)
{
if ((fromAddr == null) || (fromAddr.length() == 0)) return false;
// find the users with this email address
Collection<User> users = userDirectoryService.findUsersByEmail(fromAddr);
// if none found
if ((users == null) || (users.isEmpty())) return false;
// see if any of them are allowed to add
for (Iterator<User> i = users.iterator(); i.hasNext();)
{
User u = (User) i.next();
if (channel.allowAddMessage(u)) return true;
}
return false;
}
/**
* Create an attachment, adding it to the list of attachments.
*/
protected Reference createAttachment(String siteId, List attachments, String type, String fileName, byte[] body, String id)
{
// we just want the file name part - strip off any drive and path stuff
String name = FilenameUtils.getName(fileName); //Validator.getFileName(fileName);
String resourceName = Validator.escapeResourceName(fileName);
// make a set of properties to add for the new resource
ResourcePropertiesEdit props = contentHostingService.newResourceProperties();
props.addProperty(ResourceProperties.PROP_DISPLAY_NAME, name);
props.addProperty(ResourceProperties.PROP_DESCRIPTION, fileName);
// make an attachment resource for this URL
try
{
ContentResource attachment;
if (siteId == null) {
attachment = contentHostingService.addAttachmentResource(resourceName, type, body, props);
} else {
attachment = contentHostingService.addAttachmentResource(
resourceName, siteId, null, type, body, props);
}
// add a dereferencer for this to the attachments
Reference ref = entityManager.newReference(attachment.getReference());
attachments.add(ref);
M_log.debug(id + " : attachment: " + ref.getReference() + " size: " + body.length);
return ref;
}
catch (Exception any)
{
M_log.warn(id + " : exception adding attachment resource: " + name + " : " + any.toString());
return null;
}
}
/**
* Read in a stream from the mime body into a byte array
*/
protected byte[] readBody(int approxSize, InputStream stream)
{
// the size is APPROXIMATE, and is sometimes wrong -
// so read the body into a ByteArrayOutputStream
// that will grow if necessary
if (approxSize <= 0) return null;
ByteArrayOutputStream baos = new ByteArrayOutputStream(approxSize);
byte[] buff = new byte[10000];
try
{
//int lenRead = 0;
int count = 0;
while (count >= 0)
{
count = stream.read(buff, 0, buff.length);
if (count <= 0) break;
baos.write(buff, 0, count);
//lenRead += count;
}
}
catch (IOException e)
{
M_log.warn("readBody(): " + e);
}
return baos.toByteArray();
}
/**
* Breaks email messages into parts which can be saved as files (saves as attachments) or viewed as plain text (added to body of message).
*
* @param siteId
* Site associated with attachments, if any
* @param p
* The message-part embedded in a message..
* @param id
* The string containing the message's id.
* @param bodyBuf
* The string-buffers in which the plain/text and/or html/text message body is being built.
* @param bodyContentType
* The value of the Content-Type header for the mesage body.
* @param attachments
* The ReferenceVector in which references to attachments are collected.
* @param embedCount
* An Integer that counts embedded messages (outer message is zero).
* @return Value of embedCount (updated if this call processed any embedded messages).
*/
protected Integer parseParts(String siteId, Part p, String id, StringBuilder bodyBuf[],
StringBuilder bodyContentType, List attachments, Integer embedCount)
throws MessagingException, IOException
{
// increment embedded message counter
if (p instanceof Message)
{
embedCount = Integer.valueOf( embedCount.intValue() + 1 );
}
String type = p.getContentType();
// discard if content-type is unknown
if (type == null || "".equals(type))
{
M_log.warn(this+" message with unknown content-type discarded");
}
// add plain text to bodyBuf[0]
else if (p.isMimeType("text/plain") && p.getFileName() == null)
{
Object o = null;
// let them convert to text if possible
// but if bad encaps get the stream and do it ourselves
try {
o = p.getContent();
} catch (java.io.UnsupportedEncodingException ignore) {
o = p.getInputStream();
}
String txt = null;
String innerContentType = p.getContentType();
if (o instanceof String)
{
txt = (String) p.getContent();
if (bodyContentType != null && bodyContentType.length() == 0)
bodyContentType.append(innerContentType);
}
else if (o instanceof InputStream)
{
InputStream in = (InputStream) o;
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[in.available()];
for (int len = in.read(buf); len != -1; len = in.read(buf))
out.write(buf, 0, len);
String charset = (new ContentType(innerContentType)).getParameter("charset");
// RFC 2045 says if no char set specified use US-ASCII.
// If specified but illegal that's less clear. The common case is X-UNKNOWN.
// my sense is that UTF-8 is most likely these days but the sample we got
// was actually ISO 8859-1. Could also justify using US-ASCII. Duh...
if (charset == null)
charset = "us-ascii";
try {
txt = out.toString(MimeUtility.javaCharset(charset));
} catch (java.io.UnsupportedEncodingException ignore) {
txt = out.toString("UTF-8");
}
if (bodyContentType != null && bodyContentType.length() == 0)
bodyContentType.append(innerContentType);
}
// remove extra line breaks added by mac Mail, perhaps others
// characterized by a space followed by a line break
if (txt != null)
{
txt = txt.replaceAll(" \n", " ");
}
// make sure previous message parts ended with newline
if (bodyBuf[0].length() > 0 && bodyBuf[0].charAt(bodyBuf[0].length() - 1) != '\n')
bodyBuf[0].append("\n");
bodyBuf[0].append(txt);
}
// add html text to bodyBuf[1]
else if (p.isMimeType("text/html") && p.getFileName() == null)
{
Object o = null;
// let them convert to text if possible
// but if bad encaps get the stream and do it ourselves
try {
o = p.getContent();
} catch (java.io.UnsupportedEncodingException ignore) {
o = p.getInputStream();
}
String txt = null;
String innerContentType = p.getContentType();
if (o instanceof String)
{
txt = (String) p.getContent();
if (bodyContentType != null && bodyContentType.length() == 0)
bodyContentType.append(innerContentType);
}
else if (o instanceof InputStream)
{
InputStream in = (InputStream) o;
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buf = new byte[in.available()];
for (int len = in.read(buf); len != -1; len = in.read(buf))
out.write(buf, 0, len);
String charset = (new ContentType(innerContentType)).getParameter("charset");
if (charset == null)
charset = "us-ascii";
try {
txt = out.toString(MimeUtility.javaCharset(charset));
} catch (java.io.UnsupportedEncodingException ignore) {
txt = out.toString("UTF-8");
}
if (bodyContentType != null && bodyContentType.length() == 0)
bodyContentType.append(innerContentType);
}
// remove bad image tags and naughty javascript
if (txt !=null)
{
txt = Web.cleanHtml(txt);
}
bodyBuf[1].append(txt);
}
// process subparts of multiparts
else if (p.isMimeType("multipart/*"))
{
Multipart mp = (Multipart) p.getContent();
int count = mp.getCount();
for (int i = 0; i < count; i++)
{
embedCount = parseParts(siteId, mp.getBodyPart(i), id, bodyBuf, bodyContentType, attachments, embedCount);
}
}
// Discard parts with mime-type application/applefile. If an e-mail message contains an attachment is sent from
// a macintosh, you may get two parts, one for the data fork and one for the resource fork. The part that
// corresponds to the resource fork confuses users, this has mime-type application/applefile. The best thing
// is to discard it.
else if (p.isMimeType("application/applefile"))
{
M_log.warn(this+" message with application/applefile discarded");
}
// discard enriched text version of the message.
// Sakai only uses the plain/text or html/text version of the message.
else if (p.isMimeType("text/enriched") && p.getFileName() == null)
{
M_log.warn(this+" message with text/enriched discarded");
}
// everything else gets treated as an attachment
else
{
String name = p.getFileName();
// look for filenames not parsed by getFileName()
if ( name == null && type.indexOf(NAME_PREFIX) != -1 )
{
name = type.substring( type.indexOf(NAME_PREFIX)+NAME_PREFIX.length() );
}
// ContentType can't handle filenames with spaces or UTF8 characters
if ( name != null )
{
String decodedName = MimeUtility.decodeText( name ); // first decode RFC 2047
type = type.replace( name, URLEncoder.encode(decodedName, "UTF-8") );
name = decodedName;
}
ContentType cType = new ContentType(type);
String disposition = p.getDisposition();
int approxSize = p.getSize();
if (name == null)
{
name = "unknown";
// if file's parent is multipart/alternative,
// provide a better name for the file
if (p instanceof BodyPart)
{
Multipart parent = ((BodyPart) p).getParent();
if (parent != null)
{
String pType = parent.getContentType();
ContentType pcType = new ContentType(pType);
if (pcType.getBaseType().equalsIgnoreCase("multipart/alternative"))
{
name = "message" + embedCount;
}
}
}
if (p.isMimeType("text/html"))
{
name += ".html";
}
else if (p.isMimeType("text/richtext"))
{
name += ".rtx";
}
else if (p.isMimeType("text/rtf"))
{
name += ".rtf";
}
else if (p.isMimeType("text/enriched"))
{
name += ".etf";
}
else if (p.isMimeType("text/plain"))
{
name += ".txt";
}
else if (p.isMimeType("text/xml"))
{
name += ".xml";
}
else if (p.isMimeType("message/rfc822"))
{
name += ".txt";
}
}
// read the attachments bytes, and create it as an attachment in content hosting
byte[] bodyBytes = readBody(approxSize, p.getInputStream());
if ((bodyBytes != null) && (bodyBytes.length > 0))
{
// can we ignore the attachment it it's just whitespace chars??
Reference attachment = createAttachment(siteId, attachments, cType.getBaseType(), name, bodyBytes, id);
// add plain/text attachment reference (if plain/text message)
if (attachment != null && bodyBuf[0].length() > 0)
bodyBuf[0].append("[see attachment: \"" + name + "\", size: " + bodyBytes.length + " bytes]\n\n");
// add html/text attachment reference (if html/text message)
if (attachment != null && bodyBuf[1].length() > 0)
bodyBuf[1].append("<p>[see attachment: \"" + name + "\", size: " + bodyBytes.length + " bytes]</p>");
// add plain/text attachment reference (if no plain/text and no html/text)
if (attachment != null && bodyBuf[0].length() == 0 && bodyBuf[1].length() == 0)
bodyBuf[0].append("[see attachment: \"" + name + "\", size: " + bodyBytes.length + " bytes]\n\n");
}
}
return embedCount;
}
}