http://git-wip-us.apache.org/repos/asf/james-project/blob/936746b9/server/container/core/src/main/java/org/apache/james/server/core/MailImpl.java ---------------------------------------------------------------------- diff --git a/server/container/core/src/main/java/org/apache/james/server/core/MailImpl.java b/server/container/core/src/main/java/org/apache/james/server/core/MailImpl.java new file mode 100644 index 0000000..106d46f --- /dev/null +++ b/server/container/core/src/main/java/org/apache/james/server/core/MailImpl.java @@ -0,0 +1,691 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 org.apache.james.server.core; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OptionalDataException; +import java.io.OutputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.UUID; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.ParseException; + +import org.apache.james.lifecycle.api.Disposable; +import org.apache.james.lifecycle.api.LifecycleUtil; +import org.apache.james.core.MailAddress; +import org.apache.mailet.Mail; +import org.apache.mailet.PerRecipientHeaders; +import org.apache.mailet.PerRecipientHeaders.Header; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * <p> + * Wraps a MimeMessage adding routing information (from SMTP) and some simple + * API enhancements. + * </p> + * <p> + * From James version > 2.2.0a8 "mail attributes" have been added. Backward and + * forward compatibility is supported: + * <ul> + * <li>messages stored in file repositories <i>without</i> attributes by James + * version <= 2.2.0a8 will be processed by later versions as having an empty + * attributes hashmap;</li> + * <li>messages stored in file repositories <i>with</i> attributes by James + * version > 2.2.0a8 will be processed by previous versions, ignoring the + * attributes.</li> + * </ul> + * </p> + */ +public class MailImpl implements Disposable, Mail { + + private static final Logger LOGGER = LoggerFactory.getLogger(MailImpl.class); + + /** + * We hardcode the serialVersionUID so that from James 1.2 on, MailImpl will + * be deserializable (so your mail doesn't get lost) + */ + public static final long serialVersionUID = -4289663364703986260L; + /** + * The error message, if any, associated with this mail. + */ + private String errorMessage; + /** + * The state of this mail, which determines how it is processed. + */ + private String state; + /** + * The MimeMessage that holds the mail data. + */ + private MimeMessage message; + /** + * The sender of this mail. + */ + private MailAddress sender; + /** + * The collection of recipients to whom this mail was sent. + */ + private Collection<MailAddress> recipients; + /** + * The identifier for this mail message + */ + private String name; + /** + * The remote host from which this mail was sent. + */ + private String remoteHost = "localhost"; + /** + * The remote address from which this mail was sent. + */ + private String remoteAddr = "127.0.0.1"; + /** + * The last time this message was updated. + */ + private Date lastUpdated = new Date(); + /** + * Attributes added to this MailImpl instance + */ + private Map<String, Object> attributes; + /** + * Specific headers for some recipients + * These headers will be added at delivery time + */ + private PerRecipientHeaders perRecipientSpecificHeaders; + + /** + * A constructor that creates a new, uninitialized MailImpl + */ + public MailImpl() { + setState(Mail.DEFAULT); + attributes = new HashMap<>(); + perRecipientSpecificHeaders = new PerRecipientHeaders(); + } + + /** + * A constructor that creates a MailImpl with the specified name, sender, + * and recipients. + * + * @param name the name of the MailImpl + * @param sender the sender for this MailImpl + * @param recipients the collection of recipients of this MailImpl + */ + public MailImpl(String name, MailAddress sender, Collection<MailAddress> recipients) { + this(); + this.name = name; + this.sender = sender; + this.recipients = null; + + // Copy the recipient list + if (recipients != null) { + this.recipients = new ArrayList<>(); + this.recipients.addAll(recipients); + } + } + + /** + * Create a copy of the input mail and assign it a new name + * + * @param mail original mail + * @throws MessagingException when the message is not clonable + */ + public MailImpl(Mail mail) throws MessagingException { + this(mail, newName(mail)); + } + + /** + * @param mail + * @param newName + * @throws MessagingException + */ + @SuppressWarnings("unchecked") + public MailImpl(Mail mail, String newName) throws MessagingException { + this(newName, mail.getSender(), mail.getRecipients(), mail.getMessage()); + setRemoteHost(mail.getRemoteHost()); + setRemoteAddr(mail.getRemoteAddr()); + setLastUpdated(mail.getLastUpdated()); + try { + if (mail instanceof MailImpl) { + setAttributesRaw((HashMap<String, Object>) cloneSerializableObject(((MailImpl) mail).getAttributesRaw())); + } else { + HashMap<String, Object> attribs = new HashMap<>(); + for (Iterator<String> i = mail.getAttributeNames(); i.hasNext(); ) { + String hashKey = i.next(); + attribs.put(hashKey, cloneSerializableObject(mail.getAttribute(hashKey))); + } + setAttributesRaw(attribs); + } + } catch (IOException | ClassNotFoundException e) { + LOGGER.error("Error while deserializing attributes", e); + setAttributesRaw(new HashMap<>()); + } + } + + /** + * A constructor that creates a MailImpl with the specified name, sender, + * recipients, and message data. + * + * @param name the name of the MailImpl + * @param sender the sender for this MailImpl + * @param recipients the collection of recipients of this MailImpl + * @param messageIn a stream containing the message source + */ + public MailImpl(String name, MailAddress sender, Collection<MailAddress> recipients, InputStream messageIn) throws MessagingException { + this(name, sender, recipients); + MimeMessageSource source = new MimeMessageInputStreamSource(name, messageIn); + // if MimeMessageCopyOnWriteProxy throws an error in the constructor we + // have to manually care disposing our source. + try { + this.setMessage(new MimeMessageCopyOnWriteProxy(source)); + } catch (MessagingException e) { + LifecycleUtil.dispose(source); + throw e; + } + } + + /** + * A constructor that creates a MailImpl with the specified name, sender, + * recipients, and MimeMessage. + * + * @param name the name of the MailImpl + * @param sender the sender for this MailImpl + * @param recipients the collection of recipients of this MailImpl + * @param message the MimeMessage associated with this MailImpl + */ + public MailImpl(String name, MailAddress sender, Collection<MailAddress> recipients, MimeMessage message) { + this(name, sender, recipients); + this.setMessage(new MimeMessageCopyOnWriteProxy(message)); + } + + /** + * Duplicate the MailImpl. + * + * @return a MailImpl that is a duplicate of this one + */ + public Mail duplicate() { + return duplicate(name); + } + + /** + * Duplicate the MailImpl, replacing the mail name with the one passed in as + * an argument. + * + * @param newName the name for the duplicated mail + * @return a MailImpl that is a duplicate of this one with a different name + */ + public Mail duplicate(String newName) { + try { + return new MailImpl(this, newName); + } catch (MessagingException me) { + // Ignored. Return null in the case of an error. + } + return null; + } + + /** + * Get the error message associated with this MailImpl. + * + * @return the error message associated with this MailImpl + */ + @Override + public String getErrorMessage() { + return errorMessage; + } + + /** + * Get the MimeMessage associated with this MailImpl. + * + * @return the MimeMessage associated with this MailImpl + */ + @Override + public MimeMessage getMessage() throws MessagingException { + return message; + } + + /** + * Set the name of this MailImpl. + * + * @param name the name of this MailImpl + */ + @Override + public void setName(String name) { + this.name = name; + } + + /** + * Get the name of this MailImpl. + * + * @return the name of this MailImpl + */ + @Override + public String getName() { + return name; + } + + /** + * Get the recipients of this MailImpl. + * + * @return the recipients of this MailImpl + */ + @Override + public Collection<MailAddress> getRecipients() { + return recipients; + } + + /** + * Get the sender of this MailImpl. + * + * @return the sender of this MailImpl + */ + @Override + public MailAddress getSender() { + return sender; + } + + /** + * Get the state of this MailImpl. + * + * @return the state of this MailImpl + */ + @Override + public String getState() { + return state; + } + + /** + * Get the remote host associated with this MailImpl. + * + * @return the remote host associated with this MailImpl + */ + @Override + public String getRemoteHost() { + return remoteHost; + } + + /** + * Get the remote address associated with this MailImpl. + * + * @return the remote address associated with this MailImpl + */ + @Override + public String getRemoteAddr() { + return remoteAddr; + } + + /** + * Get the last updated time for this MailImpl. + * + * @return the last updated time for this MailImpl + */ + @Override + public Date getLastUpdated() { + return lastUpdated; + } + + /** + * <p> + * Return the size of the message including its headers. + * MimeMessage.getSize() method only returns the size of the message body. + * </p> + * <p/> + * <p> + * Note: this size is not guaranteed to be accurate - see Sun's + * documentation of MimeMessage.getSize(). + * </p> + * + * @return approximate size of full message including headers. + * @throws MessagingException if a problem occurs while computing the message size + */ + @Override + public long getMessageSize() throws MessagingException { + return MimeMessageUtil.getMessageSize(message); + } + + /** + * Set the error message associated with this MailImpl. + * + * @param msg the new error message associated with this MailImpl + */ + @Override + public void setErrorMessage(String msg) { + this.errorMessage = msg; + } + + /** + * Set the MimeMessage associated with this MailImpl. + * + * @param message the new MimeMessage associated with this MailImpl + */ + @Override + public void setMessage(MimeMessage message) { + + // TODO: We should use the MimeMessageCopyOnWriteProxy + // everytime we set the MimeMessage. We should + // investigate if we should wrap it here + + if (this.message != message) { + // If a setMessage is called on a Mail that already have a message + // (discouraged) we have to make sure that the message we remove is + // correctly unreferenced and disposed, otherwise it will keep locks + if (this.message != null) { + LifecycleUtil.dispose(this.message); + } + this.message = message; + } + } + + /** + * Set the recipients for this MailImpl. + * + * @param recipients the recipients for this MailImpl + */ + @Override + public void setRecipients(Collection<MailAddress> recipients) { + this.recipients = recipients; + } + + /** + * Set the sender of this MailImpl. + * + * @param sender the sender of this MailImpl + */ + public void setSender(MailAddress sender) { + this.sender = sender; + } + + /** + * Set the state of this MailImpl. + * + * @param state the state of this MailImpl + */ + public void setState(String state) { + this.state = state; + } + + /** + * Set the remote address associated with this MailImpl. + * + * @param remoteHost the new remote host associated with this MailImpl + */ + public void setRemoteHost(String remoteHost) { + this.remoteHost = remoteHost; + } + + /** + * Set the remote address associated with this MailImpl. + * + * @param remoteAddr the new remote address associated with this MailImpl + */ + public void setRemoteAddr(String remoteAddr) { + this.remoteAddr = remoteAddr; + } + + /** + * Set the date this mail was last updated. + * + * @param lastUpdated the date the mail was last updated + */ + public void setLastUpdated(Date lastUpdated) { + // Make a defensive copy to ensure that the date + // doesn't get changed external to the class + if (lastUpdated != null) { + lastUpdated = new Date(lastUpdated.getTime()); + } + this.lastUpdated = lastUpdated; + } + + /** + * Writes the message out to an OutputStream. + * + * @param out the OutputStream to which to write the content + * @throws MessagingException if the MimeMessage is not set for this MailImpl + * @throws IOException if an error occurs while reading or writing from the stream + */ + public void writeMessageTo(OutputStream out) throws IOException, MessagingException { + if (message != null) { + message.writeTo(out); + } else { + throw new MessagingException("No message set for this MailImpl."); + } + } + + // Serializable Methods + // TODO: These need some work. Currently very tightly coupled to + // the internal representation. + + /** + * Read the MailImpl from an <code>ObjectInputStream</code>. + * + * @param in the ObjectInputStream from which the object is read + * @throws IOException if an error occurs while reading from the stream + * @throws ClassNotFoundException ? + * @throws ClassCastException if the serialized objects are not of the appropriate type + */ + @SuppressWarnings("unchecked") + private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { + try { + Object obj = in.readObject(); + if (obj == null) { + sender = null; + } else if (obj instanceof String) { + sender = new MailAddress((String) obj); + } else if (obj instanceof MailAddress) { + sender = (MailAddress) obj; + } + } catch (ParseException pe) { + throw new IOException("Error parsing sender address: " + pe.getMessage()); + } + recipients = (Collection<MailAddress>) in.readObject(); + state = (String) in.readObject(); + errorMessage = (String) in.readObject(); + name = (String) in.readObject(); + remoteHost = (String) in.readObject(); + remoteAddr = (String) in.readObject(); + setLastUpdated((Date) in.readObject()); + // the following is under try/catch to be backwards compatible + // with messages created with James version <= 2.2.0a8 + try { + attributes = (HashMap<String, Object>) in.readObject(); + } catch (OptionalDataException ode) { + if (ode.eof) { + attributes = new HashMap<>(); + } else { + throw ode; + } + } + } + + /** + * Write the MailImpl to an <code>ObjectOutputStream</code>. + * + * @param out the ObjectOutputStream to which the object is written + * @throws IOException if an error occurs while writing to the stream + */ + private void writeObject(java.io.ObjectOutputStream out) throws IOException { + out.writeObject(sender); + out.writeObject(recipients); + out.writeObject(state); + out.writeObject(errorMessage); + out.writeObject(name); + out.writeObject(remoteHost); + out.writeObject(remoteAddr); + out.writeObject(lastUpdated); + out.writeObject(attributes); + } + + @Override + public void dispose() { + LifecycleUtil.dispose(message); + message = null; + } + + /** + * <p> + * This method is necessary, when Mail repositories needs to deal explicitly + * with storing Mail attributes as a Serializable + * </p> + * <p> + * <strong>Note</strong>: This method is not exposed in the Mail interface, + * it is for internal use by James only. + * </p> + * + * @return Serializable of the entire attributes collection + * @since 2.2.0 + */ + public Map<String, Object> getAttributesRaw() { + return attributes; + } + + /** + * <p> + * This method is necessary, when Mail repositories needs to deal explicitly + * with retriving Mail attributes as a Serializable + * </p> + * <p> + * <strong>Note</strong>: This method is not exposed in the Mail interface, + * it is for internal use by James only. + * </p> + * + * @param attr Serializable of the entire attributes collection + * @since 2.2.0 + */ + public void setAttributesRaw(HashMap<String, Object> attr) { + this.attributes = (attr == null) ? new HashMap<>() : attr; + } + + @Override + public Serializable getAttribute(String key) { + return (Serializable) attributes.get(key); + } + + @Override + public Serializable setAttribute(String key, Serializable object) { + return (Serializable) attributes.put(key, object); + } + + @Override + public Serializable removeAttribute(String key) { + return (Serializable) attributes.remove(key); + } + + @Override + public void removeAllAttributes() { + attributes.clear(); + } + + @Override + public Iterator<String> getAttributeNames() { + return attributes.keySet().iterator(); + } + + @Override + public boolean hasAttributes() { + return !attributes.isEmpty(); + } + + /** + * This methods provide cloning for serializable objects. Mail Attributes + * are Serializable but not Clonable so we need a deep copy + * + * @param o Object to be cloned + * @return the cloned Object + * @throws IOException + * @throws ClassNotFoundException + */ + private static Object cloneSerializableObject(Object o) throws IOException, ClassNotFoundException { + ByteArrayOutputStream b = new ByteArrayOutputStream(); + ObjectOutputStream out = new ObjectOutputStream(b); + out.writeObject(o); + out.flush(); + out.close(); + ByteArrayInputStream bi = new ByteArrayInputStream(b.toByteArray()); + ObjectInputStream in = new ObjectInputStream(bi); + return in.readObject(); + } + + private static final java.util.Random random = new java.util.Random(); // Used + // to + // generate + // new + // mail + // names + + /** + * Create a unique new primary key name for the given MailObject. + * + * @param mail the mail to use as the basis for the new mail name + * @return a new name + */ + public static String newName(Mail mail) throws MessagingException { + String oldName = mail.getName(); + + // Checking if the original mail name is too long, perhaps because of a + // loop caused by a configuration error. + // it could cause a "null pointer exception" in AvalonMailRepository + // much + // harder to understand. + if (oldName.length() > 76) { + int count = 0; + int index = 0; + while ((index = oldName.indexOf('!', index + 1)) >= 0) { + count++; + } + // It looks like a configuration loop. It's better to stop. + if (count > 7) { + throw new MessagingException("Unable to create a new message name: too long." + " Possible loop in config.xml."); + } else { + oldName = oldName.substring(0, 76); + } + } + + return oldName + "-!" + random.nextInt(1048576); + } + + /** + * Generate a new identifier/name for a mail being processed by this server. + * + * @return the new identifier + */ + public static String getId() { + return "Mail" + System.currentTimeMillis() + "-" + UUID.randomUUID(); + } + + @Override + public PerRecipientHeaders getPerRecipientSpecificHeaders() { + return perRecipientSpecificHeaders; + } + + @Override + public void addSpecificHeaderForRecipient(Header header, MailAddress recipient) { + perRecipientSpecificHeaders.addHeaderForRecipient(header, recipient); + } +}
http://git-wip-us.apache.org/repos/asf/james-project/blob/936746b9/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageCopyOnWriteProxy.java ---------------------------------------------------------------------- diff --git a/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageCopyOnWriteProxy.java b/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageCopyOnWriteProxy.java new file mode 100644 index 0000000..53483eb --- /dev/null +++ b/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageCopyOnWriteProxy.java @@ -0,0 +1,561 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 org.apache.james.server.core; + +import javax.activation.DataHandler; +import javax.mail.Address; +import javax.mail.Flags; +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.Session; +import javax.mail.Flags.Flag; +import javax.mail.internet.MimeMessage; +import javax.mail.search.SearchTerm; + +import org.apache.james.lifecycle.api.Disposable; +import org.apache.james.lifecycle.api.LifecycleUtil; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Date; +import java.util.Enumeration; + +/** + * This object wraps a "possibly shared" MimeMessage tracking copies and + * automatically cloning it (if shared) when a write operation is invoked. + */ +public class MimeMessageCopyOnWriteProxy extends MimeMessage implements Disposable { + + /** + * Used internally to track the reference count It is important that this is + * static otherwise it will keep a reference to the parent object. + */ + protected static class MessageReferenceTracker { + + /** + * reference counter + */ + private int referenceCount = 1; + + /** + * The mime message in memory + */ + private MimeMessage wrapped = null; + + public MessageReferenceTracker(MimeMessage ref) { + wrapped = ref; + } + + protected synchronized void incrementReferenceCount() { + referenceCount++; + } + + protected synchronized void decrementReferenceCount() { + referenceCount--; + if (referenceCount <= 0) { + LifecycleUtil.dispose(wrapped); + wrapped = null; + } + } + + protected synchronized int getReferenceCount() { + return referenceCount; + } + + public synchronized MimeMessage getWrapped() { + return wrapped; + } + + } + + protected MessageReferenceTracker refCount; + + public MimeMessageCopyOnWriteProxy(MimeMessage original) { + this(original, false); + } + + public MimeMessageCopyOnWriteProxy(MimeMessageSource original) throws MessagingException { + this(new MimeMessageWrapper(original), true); + } + + /** + * Private constructor providing an external reference counter. + */ + private MimeMessageCopyOnWriteProxy(MimeMessage original, boolean writeable) { + super(Session.getDefaultInstance(System.getProperties(), null)); + + if (original instanceof MimeMessageCopyOnWriteProxy) { + refCount = ((MimeMessageCopyOnWriteProxy) original).refCount; + } else { + refCount = new MessageReferenceTracker(original); + } + + if (!writeable) { + refCount.incrementReferenceCount(); + } + } + + /** + * Check the number of references over the MimeMessage and clone it if + * needed before returning the reference + * + * @throws MessagingException + * exception + */ + protected synchronized MimeMessage getWrappedMessageForWriting() throws MessagingException { + if (refCount.getReferenceCount() > 1) { + refCount.decrementReferenceCount(); + refCount = new MessageReferenceTracker(new MimeMessageWrapper(refCount.getWrapped())); + } + return refCount.getWrapped(); + } + + /** + * Return wrapped mimeMessage + * + * @return wrapped return the wrapped mimeMessage + */ + public synchronized MimeMessage getWrappedMessage() { + return refCount.getWrapped(); + } + + @Override + public synchronized void dispose() { + if (refCount != null) { + refCount.decrementReferenceCount(); + refCount = null; + } + } + + @Override + public void writeTo(OutputStream os) throws IOException, MessagingException { + getWrappedMessage().writeTo(os); + } + + /** + * Rewritten for optimization purposes + */ + @Override + public void writeTo(OutputStream os, String[] ignoreList) throws IOException, MessagingException { + getWrappedMessage().writeTo(os, ignoreList); + } + + /* + * Various reader methods + */ + + @Override + public Address[] getFrom() throws MessagingException { + return getWrappedMessage().getFrom(); + } + + @Override + public Address[] getRecipients(Message.RecipientType type) throws MessagingException { + return getWrappedMessage().getRecipients(type); + } + + @Override + public Address[] getAllRecipients() throws MessagingException { + return getWrappedMessage().getAllRecipients(); + } + + @Override + public Address[] getReplyTo() throws MessagingException { + return getWrappedMessage().getReplyTo(); + } + + @Override + public String getSubject() throws MessagingException { + return getWrappedMessage().getSubject(); + } + + @Override + public Date getSentDate() throws MessagingException { + return getWrappedMessage().getSentDate(); + } + + @Override + public Date getReceivedDate() throws MessagingException { + return getWrappedMessage().getReceivedDate(); + } + + @Override + public int getSize() throws MessagingException { + return getWrappedMessage().getSize(); + } + + @Override + public int getLineCount() throws MessagingException { + return getWrappedMessage().getLineCount(); + } + + @Override + public String getContentType() throws MessagingException { + return getWrappedMessage().getContentType(); + } + + @Override + public boolean isMimeType(String mimeType) throws MessagingException { + return getWrappedMessage().isMimeType(mimeType); + } + + @Override + public String getDisposition() throws MessagingException { + return getWrappedMessage().getDisposition(); + } + + @Override + public String getEncoding() throws MessagingException { + return getWrappedMessage().getEncoding(); + } + + @Override + public String getContentID() throws MessagingException { + return getWrappedMessage().getContentID(); + } + + @Override + public String getContentMD5() throws MessagingException { + return getWrappedMessage().getContentMD5(); + } + + @Override + public String getDescription() throws MessagingException { + return getWrappedMessage().getDescription(); + } + + @Override + public String[] getContentLanguage() throws MessagingException { + return getWrappedMessage().getContentLanguage(); + } + + @Override + public String getMessageID() throws MessagingException { + return getWrappedMessage().getMessageID(); + } + + @Override + public String getFileName() throws MessagingException { + return getWrappedMessage().getFileName(); + } + + @Override + public InputStream getInputStream() throws IOException, MessagingException { + return getWrappedMessage().getInputStream(); + } + + @Override + public DataHandler getDataHandler() throws MessagingException { + return getWrappedMessage().getDataHandler(); + } + + @Override + public Object getContent() throws IOException, MessagingException { + return getWrappedMessage().getContent(); + } + + @Override + public String[] getHeader(String name) throws MessagingException { + return getWrappedMessage().getHeader(name); + } + + @Override + public String getHeader(String name, String delimiter) throws MessagingException { + return getWrappedMessage().getHeader(name, delimiter); + } + + @SuppressWarnings("unchecked") + @Override + public Enumeration<String> getAllHeaders() throws MessagingException { + return getWrappedMessage().getAllHeaders(); + } + + @SuppressWarnings("unchecked") + @Override + public Enumeration<String> getMatchingHeaders(String[] names) throws MessagingException { + return getWrappedMessage().getMatchingHeaders(names); + } + + @SuppressWarnings("unchecked") + @Override + public Enumeration<String> getNonMatchingHeaders(String[] names) throws MessagingException { + return getWrappedMessage().getNonMatchingHeaders(names); + } + + @SuppressWarnings("unchecked") + @Override + public Enumeration<String> getAllHeaderLines() throws MessagingException { + return getWrappedMessage().getAllHeaderLines(); + } + + @SuppressWarnings("unchecked") + @Override + public Enumeration<String> getMatchingHeaderLines(String[] names) throws MessagingException { + return getWrappedMessage().getMatchingHeaderLines(names); + } + + @SuppressWarnings("unchecked") + @Override + public Enumeration<String> getNonMatchingHeaderLines(String[] names) throws MessagingException { + return getWrappedMessage().getNonMatchingHeaderLines(names); + } + + @Override + public Flags getFlags() throws MessagingException { + return getWrappedMessage().getFlags(); + } + + @Override + public boolean isSet(Flags.Flag flag) throws MessagingException { + return getWrappedMessage().isSet(flag); + } + + @Override + public Address getSender() throws MessagingException { + return getWrappedMessage().getSender(); + } + + @Override + public boolean match(SearchTerm arg0) throws MessagingException { + return getWrappedMessage().match(arg0); + } + + @Override + public InputStream getRawInputStream() throws MessagingException { + return getWrappedMessage().getRawInputStream(); + } + + @Override + public Folder getFolder() { + return getWrappedMessage().getFolder(); + } + + @Override + public int getMessageNumber() { + return getWrappedMessage().getMessageNumber(); + } + + @Override + public boolean isExpunged() { + return getWrappedMessage().isExpunged(); + } + + @Override + public boolean equals(Object arg0) { + return getWrappedMessage().equals(arg0); + } + + @Override + public int hashCode() { + return getWrappedMessage().hashCode(); + } + + @Override + public String toString() { + return getWrappedMessage().toString(); + } + + @Override + public void setFrom(Address address) throws MessagingException { + getWrappedMessageForWriting().setFrom(address); + } + + @Override + public void setFrom() throws MessagingException { + getWrappedMessageForWriting().setFrom(); + } + + @Override + public void addFrom(Address[] addresses) throws MessagingException { + getWrappedMessageForWriting().addFrom(addresses); + } + + @Override + public void setRecipients(Message.RecipientType type, Address[] addresses) throws MessagingException { + getWrappedMessageForWriting().setRecipients(type, addresses); + } + + @Override + public void addRecipients(Message.RecipientType type, Address[] addresses) throws MessagingException { + getWrappedMessageForWriting().addRecipients(type, addresses); + } + + @Override + public void setReplyTo(Address[] addresses) throws MessagingException { + getWrappedMessageForWriting().setReplyTo(addresses); + } + + @Override + public void setSubject(String subject) throws MessagingException { + getWrappedMessageForWriting().setSubject(subject); + } + + @Override + public void setSubject(String subject, String charset) throws MessagingException { + getWrappedMessageForWriting().setSubject(subject, charset); + } + + @Override + public void setSentDate(Date d) throws MessagingException { + getWrappedMessageForWriting().setSentDate(d); + } + + @Override + public void setDisposition(String disposition) throws MessagingException { + getWrappedMessageForWriting().setDisposition(disposition); + } + + @Override + public void setContentID(String cid) throws MessagingException { + getWrappedMessageForWriting().setContentID(cid); + } + + @Override + public void setContentMD5(String md5) throws MessagingException { + getWrappedMessageForWriting().setContentMD5(md5); + } + + @Override + public void setDescription(String description) throws MessagingException { + getWrappedMessageForWriting().setDescription(description); + } + + @Override + public void setDescription(String description, String charset) throws MessagingException { + getWrappedMessageForWriting().setDescription(description, charset); + } + + @Override + public void setContentLanguage(String[] languages) throws MessagingException { + getWrappedMessageForWriting().setContentLanguage(languages); + } + + @Override + public void setFileName(String filename) throws MessagingException { + getWrappedMessageForWriting().setFileName(filename); + } + + @Override + public void setDataHandler(DataHandler dh) throws MessagingException { + getWrappedMessageForWriting().setDataHandler(dh); + } + + @Override + public void setContent(Object o, String type) throws MessagingException { + getWrappedMessageForWriting().setContent(o, type); + } + + @Override + public void setText(String text) throws MessagingException { + getWrappedMessageForWriting().setText(text); + } + + @Override + public void setText(String text, String charset) throws MessagingException { + getWrappedMessageForWriting().setText(text, charset); + } + + @Override + public void setContent(Multipart mp) throws MessagingException { + getWrappedMessageForWriting().setContent(mp); + } + + /** + * This does not need a writable message + */ + @Override + public Message reply(boolean replyToAll) throws MessagingException { + return getWrappedMessage().reply(replyToAll); + } + + @Override + public void setHeader(String name, String value) throws MessagingException { + getWrappedMessageForWriting().setHeader(name, value); + } + + @Override + public void addHeader(String name, String value) throws MessagingException { + getWrappedMessageForWriting().addHeader(name, value); + } + + @Override + public void removeHeader(String name) throws MessagingException { + getWrappedMessageForWriting().removeHeader(name); + } + + @Override + public void addHeaderLine(String line) throws MessagingException { + getWrappedMessageForWriting().addHeaderLine(line); + } + + @Override + public void setFlags(Flags flag, boolean set) throws MessagingException { + getWrappedMessageForWriting().setFlags(flag, set); + } + + @Override + public void saveChanges() throws MessagingException { + getWrappedMessageForWriting().saveChanges(); + } + + /* + * Since JavaMail 1.2 + */ + + @Override + public void addRecipients(Message.RecipientType type, String addresses) throws MessagingException { + getWrappedMessageForWriting().addRecipients(type, addresses); + } + + @Override + public void setRecipients(Message.RecipientType type, String addresses) throws MessagingException { + getWrappedMessageForWriting().setRecipients(type, addresses); + } + + @Override + public void setSender(Address arg0) throws MessagingException { + getWrappedMessageForWriting().setSender(arg0); + } + + public void addRecipient(RecipientType arg0, Address arg1) throws MessagingException { + getWrappedMessageForWriting().addRecipient(arg0, arg1); + } + + @Override + public void setFlag(Flag arg0, boolean arg1) throws MessagingException { + getWrappedMessageForWriting().setFlag(arg0, arg1); + } + + public long getMessageSize() throws MessagingException { + return MimeMessageUtil.getMessageSize(getWrappedMessage()); + } + + /** + * Since javamail 1.4 + */ + @Override + public void setText(String text, String charset, String subtype) throws MessagingException { + getWrappedMessage().setText(text, charset, subtype); + } + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/936746b9/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageInputStream.java ---------------------------------------------------------------------- diff --git a/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageInputStream.java b/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageInputStream.java new file mode 100644 index 0000000..c3e27d4 --- /dev/null +++ b/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageInputStream.java @@ -0,0 +1,127 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 org.apache.james.server.core; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; + +/** + * Provide an {@link InputStream} over an {@link MimeMessage} + */ +public class MimeMessageInputStream extends InputStream { + private final InputStream in; + + /** + * Provide an {@link InputStream} over a {@link MimeMessage}. + * + * @param message + * the message to wrap + * @param tryCast + * try to cast the {@link MimeMessage} to + * {@link MimeMessageCopyOnWriteProxy} / + * {@link MimeMessageWrapper} to do some optimized processing if + * possible + * @throws MessagingException + */ + public MimeMessageInputStream(MimeMessage message, boolean tryCast) throws MessagingException { + MimeMessage m = message; + + // check if we need to use the wrapped message + if (tryCast && m instanceof MimeMessageCopyOnWriteProxy) { + m = ((MimeMessageCopyOnWriteProxy) m).getWrappedMessage(); + } + + // check if we can use optimized operations + if (tryCast && m instanceof MimeMessageWrapper) { + in = ((MimeMessageWrapper) m).getMessageInputStream(); + } else { + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + message.writeTo(out); + in = new ByteArrayInputStream(out.toByteArray()); + } catch (IOException e1) { + throw new MessagingException("Unable to read message " + message, e1); + } + + } + + } + + /** + * Use true as tryCast parameter + * + * {@link #MimeMessageInputStream(MimeMessage, boolean)} + */ + public MimeMessageInputStream(MimeMessage message) throws MessagingException { + this(message, true); + } + + @Override + public int read() throws IOException { + return in.read(); + } + + @Override + public void close() throws IOException { + in.close(); + } + + @Override + public int available() throws IOException { + return in.available(); + } + + @Override + public void mark(int readlimit) { + in.mark(readlimit); + } + + @Override + public boolean markSupported() { + return in.markSupported(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + return in.read(b, off, len); + } + + @Override + public int read(byte[] b) throws IOException { + return in.read(b); + } + + @Override + public void reset() throws IOException { + in.reset(); + } + + @Override + public long skip(long n) throws IOException { + return in.skip(n); + } + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/936746b9/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageInputStreamSource.java ---------------------------------------------------------------------- diff --git a/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageInputStreamSource.java b/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageInputStreamSource.java new file mode 100644 index 0000000..0eea1de --- /dev/null +++ b/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageInputStreamSource.java @@ -0,0 +1,174 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 org.apache.james.server.core; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +import javax.mail.MessagingException; +import javax.mail.util.SharedByteArrayInputStream; +import javax.mail.util.SharedFileInputStream; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.output.DeferredFileOutputStream; +import org.apache.james.lifecycle.api.Disposable; + +/** + * Takes an input stream and creates a repeatable input stream source for a + * MimeMessageWrapper. It does this by completely reading the input stream and + * saving that to data to an {@link DeferredFileOutputStream} with its threshold set to 100kb + */ +public class MimeMessageInputStreamSource extends MimeMessageSource implements Disposable { + + private final List<InputStream> streams = new ArrayList<>(); + + /** + * A temporary file used to hold the message stream + */ + private DeferredFileOutputStream out; + + /** + * The full path of the temporary file + */ + private final String sourceId; + + /** + * 100kb threshold for the stream. + */ + private final static int THRESHOLD = 1024 * 100; + + /** + * Temporary directory to use + */ + private final static File TMPDIR = new File(System.getProperty("java.io.tmpdir")); + + /** + * Construct a new MimeMessageInputStreamSource from an + * <code>InputStream</code> that contains the bytes of a MimeMessage. + * + * @param key the prefix for the name of the temp file + * @param in the stream containing the MimeMessage + * @throws MessagingException if an error occurs while trying to store the stream + */ + public MimeMessageInputStreamSource(String key, InputStream in) throws MessagingException { + super(); + // We want to immediately read this into a temporary file + // Create a temp file and channel the input stream into it + try { + out = new DeferredFileOutputStream(THRESHOLD, "mimemessage-" + key, ".m64", TMPDIR); + IOUtils.copy(in, out); + sourceId = key; + } catch (IOException ioe) { + File file = out.getFile(); + if (file != null) { + FileUtils.deleteQuietly(file); + } + throw new MessagingException("Unable to retrieve the data: " + ioe.getMessage(), ioe); + } finally { + try { + if (out != null) { + out.close(); + } + } catch (IOException ioe) { + // Ignored - logging unavailable to log this non-fatal error. + } + + try { + if (in != null) { + in.close(); + } + } catch (IOException ioe) { + // Ignored - logging unavailable to log this non-fatal error. + } + + } + } + + public MimeMessageInputStreamSource(String key) { + super(); + out = new DeferredFileOutputStream(THRESHOLD, key, ".m64", TMPDIR); + sourceId = key; + } + + /** + * Returns the unique identifier of this input stream source + * + * @return the unique identifier for this MimeMessageInputStreamSource + */ + public String getSourceId() { + return sourceId; + } + + /** + * Get an input stream to retrieve the data stored in the temporary file + * + * @return a <code>BufferedInputStream</code> containing the data + */ + public synchronized InputStream getInputStream() throws IOException { + InputStream in; + if (out.isInMemory()) { + in = new SharedByteArrayInputStream(out.getData()); + } else { + in = new SharedFileInputStream(out.getFile()); + } + streams.add(in); + return in; + } + + /** + * Get the size of the temp file + * + * @return the size of the temp file + * @throws IOException if an error is encoutered while computing the size of the + * message + */ + @Override + public long getMessageSize() throws IOException { + return out.getByteCount(); + } + + public OutputStream getWritableOutputStream() { + return out; + } + + @Override + public void dispose() { + // explicit close all streams + for (InputStream stream : streams) { + IOUtils.closeQuietly(stream); + } + + if (out != null) { + IOUtils.closeQuietly(out); + File file = out.getFile(); + if (file != null) { + FileUtils.deleteQuietly(file); + file = null; + } + out = null; + } + } + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/936746b9/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageSource.java ---------------------------------------------------------------------- diff --git a/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageSource.java b/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageSource.java new file mode 100644 index 0000000..d5375a1 --- /dev/null +++ b/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageSource.java @@ -0,0 +1,84 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 org.apache.james.server.core; + +import java.io.IOException; +import java.io.InputStream; + +/** + * This defines a reusable datasource that can supply an input stream with + * MimeMessage data. This allows a MimeMessageWrapper or other classes to grab + * the underlying data. + * + * @see MimeMessageWrapper + */ +public abstract class MimeMessageSource { + + /** + * Returns a unique String ID that represents the location from where this + * file is loaded. This will be used to identify where the data is, + * primarily to avoid situations where this data would get overwritten. + * + * @return the String ID + */ + public abstract String getSourceId(); + + /** + * Get an input stream to retrieve the data stored in the datasource + * + * @return a <code>InputStream</code> containing the data + * + * @throws IOException + * if an error occurs while generating the InputStream + */ + public abstract InputStream getInputStream() throws IOException; + + /** + * Return the size of all the data. Default implementation... others can + * override to do this much faster + * + * @return the size of the data represented by this source + * @throws IOException + * if an error is encountered while computing the message size + */ + public long getMessageSize() throws IOException { + int size = 0; + InputStream in = null; + try { + in = getInputStream(); + int read; + byte[] data = new byte[1024]; + while ((read = in.read(data)) > 0) { + size += read; + } + } finally { + try { + if (in != null) { + in.close(); + } + } catch (IOException ioe) { + // Exception ignored because logging is + // unavailable + } + } + return size; + } + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/936746b9/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageUtil.java ---------------------------------------------------------------------- diff --git a/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageUtil.java b/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageUtil.java new file mode 100644 index 0000000..698ad5d --- /dev/null +++ b/server/container/core/src/main/java/org/apache/james/server/core/MimeMessageUtil.java @@ -0,0 +1,311 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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 org.apache.james.server.core; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Enumeration; + +import javax.activation.UnsupportedDataTypeException; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeUtility; + +import org.apache.commons.io.IOUtils; + +/** + * Utility class to provide optimized write methods for the various MimeMessage + * implementations. + */ +public class MimeMessageUtil { + + /** + * Convenience method to take any MimeMessage and write the headers and body + * to two different output streams + * + * @param message + * the MimeMessage reading from + * @param headerOs + * the OutputStream writting the headers to + * @param bodyOs + * the OutputStream writting the body to + * @throws IOException + * get thrown if an IO Error detected while writing to the + * streams + * @throws MessagingException + * get thrown if an error detected while reading the message + */ + public static void writeTo(MimeMessage message, OutputStream headerOs, OutputStream bodyOs) throws IOException, MessagingException { + writeTo(message, headerOs, bodyOs, null); + } + + /** + * Convenience method to take any MimeMessage and write the headers and body + * to two different output streams, with an ignore list + * + * @param message + * the MimeMessage reading from + * @param headerOs + * the OutputStream writting the headers to + * @param bodyOs + * the OutputStream writting the body to + * @param ignoreList + * the String[] which contains headers which should be ignored + * @throws IOException + * get thrown if an IO Error detected while writing to the + * streams + * @throws MessagingException + * get thrown if an error detected while reading the message + */ + public static void writeTo(MimeMessage message, OutputStream headerOs, OutputStream bodyOs, String[] ignoreList) throws IOException, MessagingException { + MimeMessage testMessage = message; + if (message instanceof MimeMessageCopyOnWriteProxy) { + MimeMessageCopyOnWriteProxy wr = (MimeMessageCopyOnWriteProxy) message; + testMessage = wr.getWrappedMessage(); + } + if (testMessage instanceof MimeMessageWrapper) { + MimeMessageWrapper wrapper = (MimeMessageWrapper) testMessage; + if (!wrapper.isModified()) { + wrapper.writeTo(headerOs, bodyOs, ignoreList); + return; + } + } + writeToInternal(message, headerOs, bodyOs, ignoreList); + } + + /** + * + * @param message + * @param headerOs + * @param bodyOs + * @param ignoreList + * @throws MessagingException + * @throws IOException + * @throws UnsupportedDataTypeException + */ + public static void writeToInternal(MimeMessage message, OutputStream headerOs, OutputStream bodyOs, String[] ignoreList) throws MessagingException, IOException { + if (message.getMessageID() == null) { + message.saveChanges(); + } + + writeHeadersTo(message, headerOs, ignoreList); + + // Write the body to the output stream + writeMessageBodyTo(message, bodyOs); + } + + /** + * Write message body of given mimeessage to the given outputStream + * + * @param message + * the MimeMessage used as input + * @param bodyOs + * the OutputStream to write the message body to + * @throws IOException + * @throws UnsupportedDataTypeException + * @throws MessagingException + */ + public static void writeMessageBodyTo(MimeMessage message, OutputStream bodyOs) throws IOException, MessagingException { + OutputStream bos; + InputStream bis; + + try { + // Get the message as a stream. This will encode + // objects as necessary, and we have some input from + // decoding an re-encoding the stream. I'd prefer the + // raw stream, but see + bos = MimeUtility.encode(bodyOs, message.getEncoding()); + bis = message.getInputStream(); + } catch (UnsupportedDataTypeException | MessagingException udte) { + /* + * If we get an UnsupportedDataTypeException try using the raw input + * stream as a "best attempt" at rendering a message. + * + * WARNING: JavaMail v1.3 getRawInputStream() returns INVALID + * (unchanged) content for a changed message. getInputStream() works + * properly, but in this case has failed due to a missing + * DataHandler. + * + * MimeMessage.getRawInputStream() may throw a "no content" + * MessagingException. In JavaMail v1.3, when you initially create a + * message using MimeMessage APIs, there is no raw content + * available. getInputStream() works, but getRawInputStream() throws + * an exception. If we catch that exception, throw the UDTE. It + * should mean that someone has locally constructed a message part + * for which JavaMail doesn't have a DataHandler. + */ + + try { + bis = message.getRawInputStream(); + bos = bodyOs; + } catch (javax.mail.MessagingException ignored) { + throw udte; + } + } + + try { + IOUtils.copy(bis, bos); + } finally { + IOUtils.closeQuietly(bis); + } + } + + /** + * Write the message headers to the given outputstream + * + * @param message + * the MimeMessage to read from + * @param headerOs + * the OutputStream to which the headers get written + * @param ignoreList + * the String[] which holds headers which should be ignored + * @throws MessagingException + */ + private static void writeHeadersTo(MimeMessage message, OutputStream headerOs, String[] ignoreList) throws MessagingException { + // Write the headers (minus ignored ones) + @SuppressWarnings("unchecked") + Enumeration<String> headers = message.getNonMatchingHeaderLines(ignoreList); + writeHeadersTo(headers, headerOs); + } + + /** + * Write the message headers to the given outputstream + * + * @param headers + * the Enumeration which holds the headers + * @param headerOs + * the OutputStream to which the headers get written + * @throws MessagingException + */ + public static void writeHeadersTo(Enumeration<String> headers, OutputStream headerOs) throws MessagingException { + try { + IOUtils.copy(new InternetHeadersInputStream(headers), headerOs); + } catch (IOException e) { + throw new MessagingException("Unable to write headers to stream", e); + } + } + + /** + * Get an InputStream which holds all headers of the given MimeMessage + * + * @param message + * the MimeMessage used as source + * @param ignoreList + * the String[] which holds headers which should be ignored + * @return stream the InputStream which holds the headers + * @throws MessagingException + */ + @SuppressWarnings("unchecked") + public static InputStream getHeadersInputStream(MimeMessage message, String[] ignoreList) throws MessagingException { + return new InternetHeadersInputStream(message.getNonMatchingHeaderLines(ignoreList)); + } + + /** + * Slow method to calculate the exact size of a message! + */ + private static final class SizeCalculatorOutputStream extends OutputStream { + long size = 0; + + public void write(int arg0) throws IOException { + size++; + } + + public long getSize() { + return size; + } + + public void write(byte[] arg0, int arg1, int arg2) throws IOException { + size += arg2; + } + + public void write(byte[] arg0) throws IOException { + size += arg0.length; + } + } + + /** + * Return the full site of an mimeMessage + * + * @return size of full message including headers + * @throws MessagingException + * if a problem occours while computing the message size + */ + public static long getMessageSize(MimeMessage message) throws MessagingException { + // If we have a MimeMessageWrapper, then we can ask it for just the + // message size and skip calculating it + long size = -1; + + if (message instanceof MimeMessageWrapper) { + MimeMessageWrapper wrapper = (MimeMessageWrapper) message; + size = wrapper.getMessageSize(); + } else if (message instanceof MimeMessageCopyOnWriteProxy) { + MimeMessageCopyOnWriteProxy wrapper = (MimeMessageCopyOnWriteProxy) message; + size = wrapper.getMessageSize(); + } + + if (size == -1) { + size = calculateMessageSize(message); + } + + return size; + } + + /** + * Calculate the size of the give mimeMessage + * + * @param message + * the MimeMessage + * @return size the calculated size + * @throws MessagingException + * if a problem occours while calculate the message size + */ + public static long calculateMessageSize(MimeMessage message) throws MessagingException { + long size; + // SK: Should probably eventually store this as a locally + // maintained value (so we don't have to load and reparse + // messages each time). + size = message.getSize(); + if (size != -1) { + Enumeration<?> e = message.getAllHeaderLines(); + if (e.hasMoreElements()) { + size += 2; + } + while (e.hasMoreElements()) { + // add 2 bytes for the CRLF + size += ((String) e.nextElement()).length() + 2; + } + } + + if (size == -1) { + SizeCalculatorOutputStream out = new SizeCalculatorOutputStream(); + try { + message.writeTo(out); + } catch (IOException e) { + // should never happen as SizeCalculator does not actually throw + // IOExceptions. + throw new MessagingException("IOException wrapped by getMessageSize", e); + } + size = out.getSize(); + } + return size; + } + +} --------------------------------------------------------------------- To unsubscribe, e-mail: server-dev-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org