/*
* @(#)MimeUtils.java 31/10/2004
*
* Copyright (c) 2004, 2005 jASEN.org
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the distribution.
*
* 3. The names of the authors may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* 4. Any modification or additions to the software must be contributed back
* to the project.
*
* 5. Any investigation or reverse engineering of source code or binary to
* enable emails to bypass the filters, and hence inflict spam and or viruses
* onto users who use or do not use jASEN could subject the perpetrator to
* criminal and or civil liability.
*
* THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JASEN.ORG,
* OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
package org.jasen.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Vector;
import javax.mail.Address;
import javax.mail.Header;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Part;
import javax.mail.internet.AddressException;
import javax.mail.internet.ContentType;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.ParseException;
import org.jasen.error.DNSException;
import org.jasen.error.ErrorHandlerBroker;
import org.jasen.error.JasenParseException;
import org.jasen.error.ParseErrorType;
import org.jasen.interfaces.DNSResolver;
import org.jasen.interfaces.InetAddressResolver;
import org.jasen.interfaces.ReceivedHeaderParser;
import org.jasen.interfaces.ReceivedHeaderParserData;
import org.jasen.io.ByteToCharUTF7Converter;
import org.jasen.net.MXRecord;
/**
* <P>
* General Mime utilities.
* </P>
* @author Jason Polites
*/
public class MimeUtils
{
/**
* Undetermined (test not yet performed)
*/
public static final int FORGERY_UNDETERMINED = -1;
/**
* Confirmed forgery
*/
public static final int FORGERY_CONFIRMED = 1;
/**
* Confirmed authentic
*/
public static final int FORGERY_REJECTED = 0;
/**
* Forgery status could not be determined with absolute certainty
*/
public static final int FORGERY_UNKNOWN = 2;
public static String[] ATTACHMENT_DISPOSITIONS = {MimeMessage.ATTACHMENT, MimeMessage.INLINE};
public static Header[] getAllHeaders(MimeMessage message) throws MessagingException {
Header[] headers = null;
Enumeration e = message.getAllHeaders();
// We have to use a Vector here because we can't get the size
Vector vHeaders = new Vector();
while (e.hasMoreElements()) {
vHeaders.add(e.nextElement());
headers = (Header[]) vHeaders.toArray(new Header[vHeaders.size()]);
}
return headers;
}
public static boolean isValidAddress(String address) {
String regex = "^[a-zA-Z][\\w\\.-]*[a-zA-Z0-9]@[a-zA-Z0-9][\\w\\.-]*[a-zA-Z0-9]\\.[a-zA-Z][a-zA-Z\\.]*[a-zA-Z]$";
return address.matches(regex);
}
/**
* Verifies the given sender address against the information in the last (most recent) received header line
* <BR><BR>
* Specifically, this does the following:
* <BR><BR>
* get domain of sender<BR>
* get IP address of last (most recent) MTA<BR>
* get hostname of last (most recent) MTA<BR>
* if (MTA IP Address resolves to MTA hostname) then<BR>
* use MTA hostname for MX IP records<BR>
* else<BR>
* use MTA IP Address<BR>
*
* get MX records for sender domain<BR>
* if(at least 1 MX record IP matches MTA IP) then valid<BR>
*
* @param parser The parser to use on the header line
* @param receivedHeaderLine The last (most recent) received header line
* @param senderAddress The From address (NOT the envelope address)
* @return True if the sender is valid, false otherwise
* @throws JasenParseException
* @throws UnknownHostException
* @throws DNSException
*/
public static boolean verifySenderAddress(DNSResolver dresolver, InetAddressResolver iresolver, ReceivedHeaderParser parser, String receivedHeaderLine, String senderAddress) throws JasenParseException, UnknownHostException, DNSException {
ReceivedHeaderParserData data = parser.parse(receivedHeaderLine, iresolver);
if(data == null) {
throw new JasenParseException("Unable to parse header.", ParseErrorType.PARSE_ERROR);
}
// Use the InetAddress cache
InetAddress receiverAddress = iresolver.getByName(data.getSenderIPAddress());
String senderDomain = getDomainFromAddress(senderAddress);
String receiverDomain = receiverAddress.getHostName();
// Get the MX records for the sender domain
MXRecord[] senderMXRecords = DNSUtils.getMXRecords(dresolver, senderDomain);
if (senderMXRecords != null && senderMXRecords.length > 0) {
// Get the root domain for both the receiver and sender
if (!DNSUtils.isIPv4Address(receiverDomain)) {
String senderRootDomain = DNSUtils.getRootDomain(data.getSenderHostName());
String receiverRootDomain = DNSUtils.getRootDomain(receiverDomain);
if (receiverRootDomain.equalsIgnoreCase(senderRootDomain)) {
// Get the MX records for the receiver domain
MXRecord[] receiverMXRecords = DNSUtils.getMXRecords(dresolver, receiverRootDomain);
// The sender address is valid if one IP matches
MXRecord senderMX = null;
MXRecord receiverMX = null;
if (senderMXRecords != null && senderMXRecords.length > 0) {
if (receiverMXRecords != null && receiverMXRecords.length > 0) {
for (int i = 0; i < senderMXRecords.length; i++) {
senderMX = senderMXRecords[i];
for (int j = 0; j < receiverMXRecords.length; j++) {
receiverMX = receiverMXRecords[j];
if (senderMX.getAddress().getHostAddress().equalsIgnoreCase(receiverMX.getAddress().getHostAddress())) {
return true;
}
}
}
}
else {
return false;
}
}
else {
return false;
}
}
else {
// The IP does not match the host. This may be a forgery but we will still test the MX
// records against the IP address
String mtaIPAddress = data.getSenderIPAddress();
for (int i = 0; i < senderMXRecords.length; i++) {
if (senderMXRecords[i].getAddress().getHostAddress().equals(mtaIPAddress)) {
return true;
}
}
}
}
else {
// We could not resolve the receiver IP address to a domain
return false;
}
}
else {
// No valid MX record for this sender
return false;
}
return false;
}
/**
* Converts an Address object to an Internet Address with strict address parsing
* @param address
* @return The parsed InternetAddress
* @throws AddressException
*/
public static InternetAddress toInternetAddress(Address address) throws AddressException {
return toInternetAddress(address, true);
}
/**
* Converts an Address object to an Internet Address
* @param address
* @param strict If true, address parsing is strict
* @return The parsed InternetAddress
* @throws AddressException
*/
public static InternetAddress toInternetAddress(Address address, boolean strict) throws AddressException {
if (address instanceof InternetAddress) {
return (InternetAddress) address;
}
else {
return InternetAddress.parse(address.toString(), strict)[0];
}
}
/**
* Determines if the loaded message is a forgery.
* <br/>
* This is done by looking at the last (most recent) received header and determining if the hostname of
* the sending server matches the hostname provided by the header information via the DNS
* @param resolver The resolver used to resolve InetAddresses
* @param message The MimeMessage to test
* @param senderAddress The envelope sender
* @param parser The parser to use to parse the "Received" header(s)
* @return An integer representing the determination. 0 = Not a forgery, 1 = Definately a forgery, 2 = Unsure or could not be determined
* @throws MessagingException
* @throws CacheException
* @throws JasenParseException
*/
public static int isForgery(InetAddressResolver resolver, MimeMessage message, String senderAddress, ReceivedHeaderParser parser) throws MessagingException, JasenParseException {
// Get the header
String[] headers = message.getHeader("Received");
if(headers != null) {
boolean hostNameIsIP = false;
String header = headers[0];
// Parse the header
ReceivedHeaderParserData data = null;
try {
data = parser.parse(header,resolver);
if(data != null) {
// Get the IPAddress of the sending server
String senderIP = data.getSenderIPAddress();
// Get the reported hostname
String senderHost = data.getSenderHostName();
// If the sender host is an IP address, but not the same as the "actual"
// IP, we have a forgery
hostNameIsIP = DNSUtils.isIPAddress(senderHost);
if(hostNameIsIP) {
if(!senderHost.equals(senderIP)) {
return FORGERY_CONFIRMED; // Forgery
}
}
try
{
// Now, attempt to get the real hostname
String realHost = resolver.getByName(senderIP).getHostName();
// If the host equals the ip, we couldn't resolve
if(realHost.endsWith(senderIP)) {
if(hostNameIsIP) {
// Probably a forgery, but we can't be sure
return FORGERY_UNKNOWN;
}
else
{
// Sender identified a host that does not match the DNS, forgery
return FORGERY_CONFIRMED;
}
}
else
{
// The host was returned, we need to get the "root" of this hostname
String rootRealHost = DNSUtils.getRootDomain(realHost);
// Now get the root of the reported host
String rootReportedHost = DNSUtils.getRootDomain(senderHost);
// If they match, we are ok
if(rootRealHost.equalsIgnoreCase(rootReportedHost)) {
return FORGERY_REJECTED;
}
else
{
// try the domain of the sender
String rootSenderHost = DNSUtils.getRootDomain(getDomainFromAddress(senderAddress));
if(rootRealHost.equalsIgnoreCase(rootSenderHost)) {
return FORGERY_REJECTED; // ok
}
else
{
return FORGERY_CONFIRMED; // forgery
}
}
}
}
catch (UnknownHostException e)
{
// No host.. must be forged
return FORGERY_CONFIRMED;
}
}
else {
// Couldn't parse
return FORGERY_UNKNOWN;
}
}
catch (JasenParseException e) {
// Can't parse the header
return FORGERY_UNKNOWN;
}
}
else
{
// no headers, can't be sure
return FORGERY_UNKNOWN;
}
}
/**
* Returns the domain component of an email address
* @param emailAddress
* @return The domain ofthe address (everything after the @)
*/
public static String getDomainFromAddress(String emailAddress) {
if (emailAddress != null) {
return emailAddress.substring(emailAddress.indexOf("@") + 1, emailAddress.length());
}
else {
return null;
}
}
public static boolean isAttachment(String disposition) {
if(disposition != null) {
return (Arrays.binarySearch(ATTACHMENT_DISPOSITIONS, disposition) > -1);
}
else
{
return false;
}
}
public static void getParts(List parts, Part p) throws MessagingException, IOException {
getParts(parts, p, null, null);
}
/**
* Gets the parts from the MimeMessage<BR>
* <BR>
* Use null disposition to ignore<BR>
* @param parts A new list to hold the parts
* @param p The current Part
* @param contentType If specified, returns only parts matching the given content type. Use null to ignore
* @param disposition If specified, returns only parts matching the given disposition. Use null to ignore
*/
public static void getParts(List parts, Part p, String contentType, String disposition) throws MessagingException, IOException {
Object content = getPartContent (p);
String currentDisposition = p.getDisposition();
String currentContentType = p.getContentType();
// now we need to check if the part was a multipart...
if (content instanceof Multipart) {
Multipart mp = (Multipart) content;
// Now we need to delve into the parts
// This call to getCount seems to throw ParseExceptions occasionally
// so we will catch that exception here so we don't lose the rest of the email...
int count = 0;
try
{
count = mp.getCount();
}
catch (Exception e)
{
// We weren't able to determine the number of parts in the multipart.
// This shouldn't ever really happen, and is usually caused when
// the MIME message is incorrectly formatted.
// For now we are just going to record, but ignore the error
ErrorHandlerBroker.getInstance().getErrorHandler().handleException(e);
}
for (int i = 0; i < count; i++) {
getParts(parts, mp.getBodyPart(i), contentType, disposition);
}
}
else
{
// check to the contentType
if (contentType == null) {
// we don't need to check
if (disposition == null) {
// no need to check
// add the part
parts.add(p);
//index++;
}
else if (currentDisposition != null && currentDisposition.startsWith(disposition)) {
// add the part
parts.add(p);
}
}
else if (p.isMimeType(contentType)) {
// Check the disposition
if (disposition == null) {
// no need to check
// add the part
parts.add(p);
}
else if (currentDisposition != null && currentDisposition.startsWith(disposition)) {
// add the part
parts.add(p);
}
}
}
}
/**
* Gets the parts which match any of the dispositions
* @param parts A list of parts
* @param contentType The content type required
* @param dispositions The content dispositions required
* @return All the parts in the message which match the given content type and disposition
*/
public static List getMultiplePartsFromList(List parts, String contentType, String[] dispositions) throws IOException, MessagingException {
List matchedParts = null;
for (int i = 0; i < dispositions.length; i++) {
if (matchedParts == null) {
matchedParts = getPartsFromList(parts, contentType, dispositions[i]);
}
else {
matchedParts.addAll(getPartsFromList(parts, contentType, dispositions[i]));
}
}
return matchedParts;
}
/**
* Gets a list of all parts which are themselves MimeMessages
* @param parts
* @return
*/
public static List getSubMessagePartsFromList(List parts) throws IOException, MessagingException {
List subMessages = null;
if(parts != null) {
Iterator m = parts.iterator();
Part part = null;
Object content = null;
while(m.hasNext()) {
part = (Part) m.next();
content = getPartContent(part);
if(content instanceof MimeMessage) {
if(subMessages == null) {
subMessages = new LinkedList();
}
subMessages.add(content);
}
}
}
return subMessages;
}
/**
* Gets all parts which are unknown. An unknown part is one which has no disposition, and is not text or html
* @param parts
* @return
*/
public static List getUnknownPartsFromList(List parts) throws IOException, MessagingException {
List unknownParts = null;
if(parts != null) {
Iterator m = parts.iterator();
Part part = null;
while(m.hasNext()) {
part = (Part) m.next();
if(!(getPartContent(part) instanceof MimeMessage)) {
if(part.getDisposition() == null && !part.isMimeType(MimeType.TEXT_PLAIN) && !part.isMimeType(MimeType.TEXT_HTML)) {
// its unknown
if(unknownParts == null) {
unknownParts = new LinkedList();
}
unknownParts.add(part);
}
}
}
}
return unknownParts;
}
/**
* Gets all parts which can be considered an attachment. This includes sub messages and unknown parts
* @param parts
* @return A List of Part objects
*/
public static List getAllAttachmentParts(List parts) throws IOException, MessagingException {
List attachments = null;
if(parts != null) {
Iterator m = parts.iterator();
Part part = null;
while(m.hasNext()) {
part = (Part) m.next();
if(getPartContent(part) instanceof MimeMessage) {
// Its a sub message
if(attachments == null) {
attachments = new LinkedList();
}
attachments.add(part);
}
else if(part.getDisposition() == null && !part.isMimeType(MimeType.TEXT_PLAIN) && !part.isMimeType(MimeType.TEXT_HTML)) {
// its unknown
if(attachments == null) {
attachments = new LinkedList();
}
attachments.add(part);
}
else if(part.getDisposition() != null && (
part.getDisposition().startsWith(MimeMessage.INLINE) ||
part.getDisposition().startsWith(MimeMessage.ATTACHMENT))) {
if(attachments == null) {
attachments = new LinkedList();
}
// It's an attachment
attachments.add(part);
}
}
}
return attachments;
}
public static Part getFirstPartFromList(List parts, String contentType, String disposition) throws MessagingException, IOException {
List list = getPartsFromList(parts, contentType, disposition);
if(list != null && list.size() > 0) {
return (Part)list.iterator().next();
}
else
{
return null;
}
}
public static List getPartsFromList(List parts, String contentType, String disposition) throws MessagingException, IOException {
LinkedList matchedParts = new LinkedList();
Part p = null;
Object content = null;
String currentDisposition = null;
String currentContentType = null;
for (int i = 0; i < parts.size(); i++) {
p = (Part) parts.get(i);
content = getPartContent (p);
//content = p.getContent();
currentDisposition = p.getDisposition();
currentContentType = p.getContentType();
if (contentType == null) {
// we don't need to check
if (disposition == null) {
// no need to check
matchedParts.add(p);
}
else if (currentDisposition != null && currentDisposition.startsWith(disposition)) {
matchedParts.add(p);
}
}
else if (p.isMimeType(contentType)) {
// Check the disposition
if (disposition == null) {
// no need to check
matchedParts.add(p);
}
else if (currentDisposition != null && currentDisposition.startsWith(disposition)) {
matchedParts.add(p);
}
}
}
return matchedParts;
}
public static Object getPartContent(Part part) throws IOException, MessagingException {
Object content = null;
if(part != null) {
try
{
String strContentType = part.getContentType();
ContentType contentType = null;
String specifiedCharset = null;
try
{
contentType = new ContentType(strContentType);
specifiedCharset = contentType.getParameter("charset");
}
catch (ParseException ignore)
{
// Ignore
}
if(specifiedCharset != null) {
specifiedCharset = specifiedCharset.toLowerCase();
if (specifiedCharset.indexOf("utf-7") > -1) {
InputStream in = part.getInputStream();
ByteArrayOutputStream out = new ByteArrayOutputStream();
IOUtils.pipe(in, out, 1024);
ByteToCharUTF7Converter btc = new ByteToCharUTF7Converter();
byte[] bytes = out.toByteArray();
char[] chars = new char[bytes.length];
btc.convert(bytes, 0, bytes.length, chars, 0, chars.length);
content = new String(chars);
}
else
{
content = part.getContent();
}
}
else
{
content = part.getContent();
}
}
catch (IOException e)
{
ErrorHandlerBroker.getInstance().getErrorHandler().handleException(e);
}
}
return content;
}
}