/****************************************************************
* 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.fetchmail;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.StringTokenizer;
import javax.mail.Address;
import javax.mail.Flags;
import javax.mail.MessagingException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.ParseException;
import org.apache.james.core.MailImpl;
import org.apache.mailet.RFC2822Headers;
import org.apache.mailet.Mail;
import org.apache.mailet.MailAddress;
/**
* <p>Class <code>MessageProcessor</code> handles the delivery of
* <code>MimeMessages</code> to the James input spool.</p>
*
* <p>Messages written to the input spool always have the following Mail
* Attributes set:</p>
* <dl>
* <dt>org.apache.james.fetchmail.taskName (java.lang.String)</dt>
* <dd>The name of the fetch task that processed the message</dd>
* <dt>org.apache.james.fetchmail.folderName (java.lang.String)</dt>
* <dd>The name of the folder from which the message was fetched</dd>
* </dl>
*
* <p>Messages written to the input spool have the following Mail Attributes
* set if the corresponding condition is satisfied:
* <dl>
* <dt>org.apache.james.fetchmail.isBlacklistedRecipient</dt>
* <dd>The recipient is in the configured blacklist</dd>
* <dt>org.apache.james.fetchmail.isMaxMessageSizeExceeded (java.lang.String)</dt>
* <dd>The message size exceeds the configured limit. An empty message is
* written to the input spool. The Mail Attribute value is a String
* representing the size of the original message in bytes.</dd>
* <dt>org.apache.james.fetchmail.isRecipientNotFound</dt>
* <dd>The recipient could not be found. Delivery is to the configured recipient.
* See the discussion of delivery to a sole intended recipient below.</dd>
* <dt>org.apache.james.fetchmail.isRemoteRecievedHeaderInvalid</dt>
* <dd>The Receieved header at the index specified by parameter
* <code>remoteReceivedHeaderIndex</code> is invalid.</dd>
* <dt>org.apache.james.fetchmail.isRemoteRecipient</dt>
* <dd>The recipient is on a remote host</dd>
* <dt>org.apache.james.fetchmail.isUserUndefined</dt>
* <dd>The recipient is on a localhost but not defined to James</dd>
* <dt>org.apache.james.fetchmail.isDefaultSenderLocalPart</dt>
* <dd>The local part of the sender address could not be obtained. The
* default value has been used.</dd>
* <dt>org.apache.james.fetchmail.isDefaultSenderDomainPart</dt>
* <dd>The domain part of the sender address could not be obtained. The
* default value has been used.</dd>
* <dt>org.apache.james.fetchmail.isDefaultRemoteAddress</dt>
* <dd>The remote address could not be determined. The default value
* (localhost/127.0.0.1)has been used.</dd>
* </dl>
*
* <p>Configuration settings -
* see <code>org.apache.james.fetchmail.ParsedConfiguration</code>
* - control the messages that are written to the James input spool, those that
* are rejected and what happens to messages that are rejected.</p>
*
* <p>Rejection processing is based on the following filters:</p>
* <dl>
* <dt>RejectRemoteRecipient</dt>
* <dd>Rejects recipients on remote hosts</dd>
* <dt>RejectBlacklistedRecipient</dt>
* <dd>Rejects recipients configured in a blacklist</dd>
* <dt>RejectUserUndefined</dt>
* <dd>Rejects recipients on local hosts who are not defined as James users</dd>
* <dt>RejectRecipientNotFound</dt>
* <dd>See the discussion of delivery to a sole intended recipient below</dd>
* <dt>RejectMaxMessageSizeExceeded</dt>
* <dd>Rejects messages whose size exceeds the configured limit</dd>
* <dt>RejectRemoteReceievedHeaderInvalid</dt>
* <dd>Rejects messages whose Received header is invalid.</dd>
* </dl>
*
* <p>Rejection processing is intentionally limited to managing the status of the
* messages that are rejected on the server from which they were fetched. View
* it as a simple automation of the manual processing an end-user would perform
* through a mail client. Messages may be marked as seen or be deleted.</p>
*
* <p>Further processing can be achieved by configuring to disable rejection for
* one or more filters. This enables Messages that would have been rejected to
* be written to the James input spool. The conditional Mail Attributes
* described above identify the filter states. The Matcher/Mailet chain can
* then be used to perform any further processing required, such as notifying
* the Postmaster and/or sender, marking the message for error processing, etc.</p>
*
* <p>Note that in the case of a message exceeding the message size limit, the
* message that is written to the input spool has no content. This enables
* configuration of a mailet notifying the sender that their mail has not been
* delivered due to its size while maintaining the purpose of the filter which is
* to avoid injecting excessively large messages into the input spool.</p>
*
* <p>Delivery is to a sole intended recipient. The recipient is determined in the
* following manner:</p>
*
* <ol>
* <li>If isIgnoreIntendedRecipient(), use the configured recipient</li>
* <li>If the Envelope contains a for: stanza, use the recipient in the stanza</li>
* <li>If the Message has a sole intended recipient, use this recipient</li>
* <li>If not rejectRecipientNotFound(), use the configured recipient</li>
* </ol>
*
* <p>If a recipient cannot be determined after these steps, the message is
* rejected.</p>
*
* <p>Every delivered message CURRENTLY has an "X-fetched-from" header added
* containing the name of the fetch task. Its primary uses are to detect bouncing
* mail and provide backwards compatibility with the fetchPop task that inserted
* this header to enable injected messages to be detected in the Matcher/Mailet
* chain. This header is DEPRECATED and WILL BE REMOVED in a future version of
* fetchmail. Use the Mail Attribute <code>org.apache.james.fetchmail.taskName</code>
* instead.
*
* <p><code>MessageProcessor</code> is as agnostic as it can be about the format
* and contents of the messages it delivers. There are no RFCs that govern its
* behavior. The most releveant RFCs relate to the exchange of messages between
* MTA servers, but not POP3 or IMAP servers which are normally end-point
* servers and not expected to re-inject mail into MTAs. None the less, the
* intent is to conform to the 'spirit' of the RFCs.
* <code>MessageProcessor</code> relies on the MTA (James in this
* implementation) to manage and validate the injected mail just as it would
* when receiving mail from an upstream MTA.</p>
*
* <p>The only correction applied by <code>MessageProcessor</code> is to correct a
* missing or partial sender address. If the sender address can not be obtained,
* the default local part and default domain part is added. If the sender domain
* part is absent, the default domain part is added.</p>
*
* <p>Mail with corrections applied to the sender address will most likely pass
* Matcher tests on the sender that they might otherwise fail. The
* Mail Attributes <code>org.apache.james.fetchmail.isDefaultSenderLocalPart</code>
* and <code>org.apache.james.fetchmail.isDefaultSenderDomainPart</code> are added
* to the injected mail to enable such mail to be detected and processed accordingly.
* </p>
*
* <p>The status of messages on the server from which they were fetched that
* cannot be injected into the input spool due to non-correctable errors is
* determined by the undeliverable configuration options.</p>
*
*/
public class MessageProcessor extends ProcessorAbstract
{
private MimeMessage fieldMessageIn;
/**
* Recipient cannot be found
*/
private boolean fieldRecipientNotFound = false;
/**
* Recipient is a local user on a local host
*/
private boolean fieldRemoteRecipient = true;
/**
* The mail's Received header at index remoteReceivedHeaderIndex is invalid.
*/
private Boolean fieldRemoteReceivedHeaderInvalid;
/**
* Recipient is not a local user
*/
private boolean fieldUserUndefined = false;
/**
* The Maximum Message has been exceeded
*/
private Boolean fieldMaxMessageSizeExceeded;
/**
* Field names for an RFC2822 compliant RECEIVED Header
*/
static final private String fieldRFC2822RECEIVEDHeaderFields =
"from by via with id for ;";
/**
* Recipient is blacklisted
*/
private boolean fieldBlacklistedRecipient = false;
/**
* The RFC2822 compliant "Received : from" domain
*/
private String fieldRemoteDomain;
/**
* The remote address derived from the remote domain
*/
private String fieldRemoteAddress;
/**
* The remote host name derived from the remote domain
*/
private String fieldRemoteHostName;
/**
* The default sender local part has been used.
*/
private boolean fieldDefaultSenderLocalPart = false;
/**
* The default sender domain part has been used.
*/
private boolean fieldDefaultSenderDomainPart = false;
/**
* The default remote address has been used.
*/
private boolean fieldDefaultRemoteAddress = false;
/**
* Constructor for MessageProcessor.
*
* @param account
*/
private MessageProcessor(Account account)
{
super(account);
}
/**
* Constructor for MessageProcessor.
*
* @param messageIn
* @param account
*/
MessageProcessor(
MimeMessage messageIn,
Account account)
{
this(account);
setMessageIn(messageIn);
}
/**
* Method process attempts to deliver a fetched message.
*
* @see org.apache.james.fetchmail.ProcessorAbstract#process()
*/
public void process() throws MessagingException
{
// Log delivery attempt
if (getLogger().isDebugEnabled())
{
StringBuffer logMessageBuffer =
new StringBuffer("Attempting delivery of message with id. ");
logMessageBuffer.append(getMessageIn().getMessageID());
getLogger().debug(logMessageBuffer.toString());
}
// Determine the intended recipient
MailAddress intendedRecipient = getIntendedRecipient();
setRecipientNotFound(null == intendedRecipient);
if (isRecipientNotFound())
{
if (isDeferRecipientNotFound())
{
String messageID = getMessageIn().getMessageID();
if (!getDeferredRecipientNotFoundMessageIDs()
.contains(messageID))
{
getDeferredRecipientNotFoundMessageIDs().add(messageID);
if (getLogger().isDebugEnabled())
{
StringBuffer messageBuffer =
new StringBuffer("Deferred processing of message for which the intended recipient could not be found. Message ID: ");
messageBuffer.append(messageID);
getLogger().debug(messageBuffer.toString());
}
return;
}
else
{
getDeferredRecipientNotFoundMessageIDs().remove(messageID);
if (getLogger().isDebugEnabled())
{
StringBuffer messageBuffer =
new StringBuffer("Processing deferred message for which the intended recipient could not be found. Message ID: ");
messageBuffer.append(messageID);
getLogger().debug(messageBuffer.toString());
}
}
}
if (isRejectRecipientNotFound())
{
rejectRecipientNotFound();
return;
}
intendedRecipient = getRecipient();
StringBuffer messageBuffer =
new StringBuffer("Intended recipient not found. Using configured recipient as new envelope recipient - ");
messageBuffer.append(intendedRecipient);
messageBuffer.append('.');
logStatusInfo(messageBuffer.toString());
}
// Set the filter states
setBlacklistedRecipient(isBlacklistedRecipient(intendedRecipient));
setRemoteRecipient(!isLocalServer(intendedRecipient));
setUserUndefined(!isLocalRecipient(intendedRecipient));
// Apply the filters. Return if rejected
if (isRejectBlacklisted() && isBlacklistedRecipient())
{
rejectBlacklistedRecipient(intendedRecipient);
return;
}
if (isRejectRemoteRecipient() && isRemoteRecipient())
{
rejectRemoteRecipient(intendedRecipient);
return;
}
if (isRejectUserUndefined() && isUserUndefined())
{
rejectUserUndefined(intendedRecipient);
return;
}
if (isRejectMaxMessageSizeExceeded()
&& isMaxMessageSizeExceeded().booleanValue())
{
rejectMaxMessageSizeExceeded(getMessageIn().getSize());
return;
}
if (isRejectRemoteReceivedHeaderInvalid()
&& isRemoteReceivedHeaderInvalid().booleanValue())
{
rejectRemoteReceivedHeaderInvalid();
return;
}
// Create the mail
// If any of the mail addresses are malformed, we will get a
// ParseException.
// If the IP address and host name for the remote domain cannot
// be found, we will get an UnknownHostException.
// In both cases, we log the problem and
// return. The message disposition is defined by the
// <undeliverable> attributes.
Mail mail = null;
try
{
mail = createMail(createMessage(), intendedRecipient);
}
catch (ParseException ex)
{
handleParseException(ex);
return;
}
catch (UnknownHostException ex)
{
handleUnknownHostException(ex);
return;
}
addMailAttributes(mail);
addErrorMessages(mail);
// If this mail is bouncing move it to the ERROR repository
if (isBouncing())
{
handleBouncing(mail);
return;
}
// OK, lets send that mail!
sendMail(mail);
}
/**
* Method rejectRemoteRecipient.
* @param recipient
* @throws MessagingException
*/
protected void rejectRemoteRecipient(MailAddress recipient)
throws MessagingException
{
// Update the flags of the received message
if (!isLeaveRemoteRecipient())
setMessageDeleted();
if (isMarkRemoteRecipientSeen())
setMessageSeen();
StringBuffer messageBuffer =
new StringBuffer("Rejected mail intended for remote recipient: ");
messageBuffer.append(recipient);
messageBuffer.append('.');
logStatusInfo(messageBuffer.toString());
return;
}
/**
* Method rejectBlacklistedRecipient.
* @param recipient
* @throws MessagingException
*/
protected void rejectBlacklistedRecipient(MailAddress recipient)
throws MessagingException
{
// Update the flags of the received message
if (!isLeaveBlacklisted())
setMessageDeleted();
if (isMarkBlacklistedSeen())
setMessageSeen();
StringBuffer messageBuffer =
new StringBuffer("Rejected mail intended for blacklisted recipient: ");
messageBuffer.append(recipient);
messageBuffer.append('.');
logStatusInfo(messageBuffer.toString());
return;
}
/**
* Method rejectRecipientNotFound.
* @throws MessagingException
*/
protected void rejectRecipientNotFound() throws MessagingException
{
// Update the flags of the received message
if (!isLeaveRecipientNotFound())
setMessageDeleted();
if (isMarkRecipientNotFoundSeen())
setMessageSeen();
StringBuffer messageBuffer =
new StringBuffer("Rejected mail for which a sole intended recipient could not be found.");
messageBuffer.append(" Recipients: ");
Address[] allRecipients = getMessageIn().getAllRecipients();
for (int i = 0; i < allRecipients.length; i++)
{
messageBuffer.append(allRecipients[i]);
messageBuffer.append(' ');
}
messageBuffer.append('.');
logStatusInfo(messageBuffer.toString());
return;
}
/**
* Method rejectUserUndefined.
* @param recipient
* @throws MessagingException
*/
protected void rejectUserUndefined(MailAddress recipient)
throws MessagingException
{
// Update the flags of the received message
if (!isLeaveUserUndefined())
setMessageDeleted();
if (isMarkUserUndefinedSeen())
setMessageSeen();
StringBuffer messageBuffer =
new StringBuffer("Rejected mail intended for undefined user: ");
messageBuffer.append(recipient);
messageBuffer.append('.');
logStatusInfo(messageBuffer.toString());
return;
}
/**
* Method rejectMaxMessageSizeExceeded.
* @param message size
* @throws MessagingException
*/
protected void rejectMaxMessageSizeExceeded(int messageSize)
throws MessagingException
{
// Update the flags of the received message
if (!isLeaveMaxMessageSizeExceeded())
setMessageDeleted();
if (isMarkMaxMessageSizeExceededSeen())
setMessageSeen();
StringBuffer messageBuffer =
new StringBuffer("Rejected mail exceeding message size limit. Message size: ");
messageBuffer.append(messageSize/1024);
messageBuffer.append("KB.");
logStatusInfo(messageBuffer.toString());
return;
}
/**
* Method rejectRemoteReceivedHeaderInvalid.
* @throws MessagingException
*/
protected void rejectRemoteReceivedHeaderInvalid()
throws MessagingException
{
// Update the flags of the received message
if (!isLeaveRemoteReceivedHeaderInvalid())
setMessageDeleted();
if (isMarkRemoteReceivedHeaderInvalidSeen())
setMessageSeen();
StringBuffer messageBuffer =
new StringBuffer("Rejected mail with an invalid Received: header at index ");
messageBuffer.append(getRemoteReceivedHeaderIndex());
messageBuffer.append(".");
logStatusInfo(messageBuffer.toString());
return;
}
/**
* <p>Method createMessage answers a new <code>MimeMessage</code> from the
* fetched message.</p>
*
* <p>If the maximum message size is exceeded, an empty message is created,
* else the new message is a copy of the received message.</p>
*
* @return MimeMessage
* @throws MessagingException
*/
protected MimeMessage createMessage() throws MessagingException
{
// Create a new messsage from the received message
MimeMessage messageOut = null;
if (isMaxMessageSizeExceeded().booleanValue())
messageOut = createEmptyMessage();
else
messageOut = new MimeMessage(getMessageIn());
// set the X-fetched headers
// Note this is still required to detect bouncing mail and
// for backwards compatibility with fetchPop
messageOut.addHeader("X-fetched-from", getFetchTaskName());
return messageOut;
}
/**
* Method createEmptyMessage answers a new
* <code>MimeMessage</code> from the fetched message with the message
* contents removed.
*
* @return MimeMessage
* @throws MessagingException
*/
protected MimeMessage createEmptyMessage()
throws MessagingException
{
// Create an empty messsage
MimeMessage messageOut = new MimeMessage(getSession());
// Propogate the headers and subject
Enumeration headersInEnum = getMessageIn().getAllHeaderLines();
while (headersInEnum.hasMoreElements())
messageOut.addHeaderLine((String) headersInEnum.nextElement());
messageOut.setSubject(getMessageIn().getSubject());
// Add empty text
messageOut.setText("");
// Save
messageOut.saveChanges();
return messageOut;
}
/**
* Method createMail creates a new <code>Mail</code>.
*
* @param message
* @param recipient
* @return Mail
* @throws MessagingException
*/
protected Mail createMail(MimeMessage message, MailAddress recipient)
throws MessagingException, UnknownHostException
{
Collection recipients = new ArrayList(1);
recipients.add(recipient);
MailImpl mail =
new MailImpl(getServer().getId(), getSender(), recipients, message);
// Ensure the mail is created with non-null remote host name and address,
// otherwise the Mailet chain may go splat!
if (getRemoteAddress() == null || getRemoteHostName() == null)
{
mail.setRemoteAddr("127.0.0.1");
mail.setRemoteHost("localhost");
setDefaultRemoteAddress(true);
logStatusInfo("Remote address could not be determined. Using localhost/127.0.0.1");
}
else
{
mail.setRemoteAddr(getRemoteAddress());
mail.setRemoteHost(getRemoteHostName());
setDefaultRemoteAddress(false);
}
if (getLogger().isDebugEnabled())
{
StringBuffer messageBuffer =
new StringBuffer("Created mail with name: ");
messageBuffer.append(mail.getName());
messageBuffer.append(", sender: ");
messageBuffer.append(mail.getSender());
messageBuffer.append(", recipients: ");
Iterator recipientIterator = mail.getRecipients().iterator();
while (recipientIterator.hasNext())
{
messageBuffer.append(recipientIterator.next());
messageBuffer.append(' ');
}
messageBuffer.append(", remote address: ");
messageBuffer.append(mail.getRemoteAddr());
messageBuffer.append(", remote host name: ");
messageBuffer.append(mail.getRemoteHost());
messageBuffer.append('.');
getLogger().debug(messageBuffer.toString());
}
return mail;
}
/**
* <p>
* Method getSender answers a <code>MailAddress</code> for the sender.
* When the sender local part and/or domain part can not be obtained
* from the mail, default values are used. The flags
* 'defaultSenderLocalPart' and 'defaultSenderDomainPart' are set
* accordingly.
* </p>
*
* @return MailAddress
* @throws MessagingException
*/
protected MailAddress getSender() throws MessagingException
{
String from = null;
InternetAddress internetAddress = null;
try {
from = ((InternetAddress) getMessageIn().getFrom()[0]).getAddress().trim();
setDefaultSenderLocalPart(false);
}
catch (Exception _) {
from = getDefaultLocalPart();
setDefaultSenderLocalPart(true);
StringBuffer buffer = new StringBuffer(32);
buffer.append("Sender localpart is absent. Using default value (");
buffer.append(getDefaultLocalPart());
buffer.append(')');
logStatusInfo(buffer.toString());
}
// Check for domain part, add default if missing
if (from.indexOf('@') < 0)
{
StringBuffer fromBuffer = new StringBuffer(from);
fromBuffer.append('@');
fromBuffer.append(getDefaultDomainName());
internetAddress = new InternetAddress(fromBuffer.toString());
setDefaultSenderDomainPart(true);
StringBuffer buffer = new StringBuffer(32);
buffer.append("Sender domain is absent. Using default value (");
buffer.append(getDefaultDomainName());
buffer.append(')');
logStatusInfo(buffer.toString());
}
else
{
internetAddress = new InternetAddress(from);
setDefaultSenderDomainPart(false);
}
return new MailAddress(internetAddress);
}
/**
* <p>Method computeRemoteDomain answers a <code>String</code> that is the
* RFC2822 compliant "Received : from" domain extracted from the message
* being processed for the remote domain that sent the message.</p>
*
* <p>Often the remote domain is the domain that sent the message to the
* host of the message store, the second "received" header, which has an
* index of 1. Other times, messages may be received by a edge mail server
* and relayed internally through one or more internal mail servers prior
* to arriving at the message store host. In these cases the index is
* 1 + the number of internal servers through which a mail passes.
* </p>
* <p>The index of the header to use is specified by the configuration
* parameter <code>RemoteReceivedHeaderIndex</code>. This is set to
* point to the received header prior to the remote mail server, the one
* prior to the edge mail server.
* </p>
* <p>"received" headers are searched starting at the specified index.
* If a domain in the "received" header is not found, successively closer
* "received" headers are tried. If a domain is not found in this way, the
* local machine is used as the domain. Finally, if the local domain cannot
* be determined, the local address 127.0.0.1 is used.
* </p>
*
* @return String An RFC2822 compliant "Received : from" domain name
*/
protected String computeRemoteDomain() throws MessagingException
{
StringBuffer domainBuffer = new StringBuffer();
String[] headers = null;
if (getRemoteReceivedHeaderIndex() > -1)
headers = getMessageIn().getHeader(RFC2822Headers.RECEIVED);
// There are RECEIVED headers if the array is not null
// and its length at is greater than 0
boolean hasHeaders = (null == headers ? false : headers.length > 0);
// If there are RECEIVED headers try and extract the domain
if (hasHeaders)
{
final String headerTokens = " \n\r";
// Search the headers for a domain
for (int headerIndex =
headers.length > getRemoteReceivedHeaderIndex()
? getRemoteReceivedHeaderIndex()
: headers.length - 1;
headerIndex >= 0 && domainBuffer.length() == 0;
headerIndex--)
{
// Find the "from" token
StringTokenizer tokenizer =
new StringTokenizer(headers[headerIndex], headerTokens);
boolean inFrom = false;
while (!inFrom && tokenizer.hasMoreTokens())
inFrom = tokenizer.nextToken().equals("from");
// Add subsequent tokens to the domain buffer until another
// field is encountered or there are no more tokens
while (inFrom && tokenizer.hasMoreTokens())
{
String token = tokenizer.nextToken();
if (inFrom =
getRFC2822RECEIVEDHeaderFields().indexOf(token) == -1)
{
domainBuffer.append(token);
domainBuffer.append(' ');
}
}
}
}
// If a domain was not found, the default is the local host and
// if we cannot resolve this, the local address 127.0.0.1
// Note that earlier versions of this code simply used 'localhost'
// which works fine with java.net but is not resolved by dnsjava
// which was introduced in v2.2.0. See Jira issue JAMES-302.
if (domainBuffer.length() == 0)
{
try
{
InetAddress addr1 = java.net.InetAddress.getLocalHost();
// These shenanigans are required to get the fully qualified
// hostname prior to JDK 1.4 in which getCanonicalHostName()
// does the job for us
InetAddress addr2 =
java.net.InetAddress.getByName(addr1.getHostAddress());
InetAddress addr3 =
java.net.InetAddress.getByName(addr2.getHostName());
domainBuffer.append(addr3.getHostName());
}
catch (UnknownHostException ue)
{
domainBuffer.append("[127.0.0.1]");
}
}
return domainBuffer.toString().trim();
}
/**
* Method handleBouncing sets the Mail state to ERROR and delete from
* the message store.
*
* @param mail
*/
protected void handleBouncing(Mail mail) throws MessagingException
{
mail.setState(Mail.ERROR);
setMessageDeleted();
mail.setErrorMessage(
"This mail from FetchMail task "
+ getFetchTaskName()
+ " seems to be bouncing!");
logStatusError("Message is bouncing! Deleted from message store and moved to the Error repository.");
}
/**
* Method handleParseException.
* @param ex
* @throws MessagingException
*/
protected void handleParseException(ParseException ex)
throws MessagingException
{
// Update the flags of the received message
if (!isLeaveUndeliverable())
setMessageDeleted();
if (isMarkUndeliverableSeen())
setMessageSeen();
logStatusWarn("Message could not be delivered due to an error parsing a mail address.");
if (getLogger().isDebugEnabled())
{
StringBuffer messageBuffer =
new StringBuffer("UNDELIVERABLE Message ID: ");
messageBuffer.append(getMessageIn().getMessageID());
getLogger().debug(messageBuffer.toString(), ex);
}
}
/**
* Method handleUnknownHostException.
* @param ex
* @throws MessagingException
*/
protected void handleUnknownHostException(UnknownHostException ex)
throws MessagingException
{
// Update the flags of the received message
if (!isLeaveUndeliverable())
setMessageDeleted();
if (isMarkUndeliverableSeen())
setMessageSeen();
logStatusWarn("Message could not be delivered due to an error determining the remote domain.");
if (getLogger().isDebugEnabled())
{
StringBuffer messageBuffer =
new StringBuffer("UNDELIVERABLE Message ID: ");
messageBuffer.append(getMessageIn().getMessageID());
getLogger().debug(messageBuffer.toString(), ex);
}
}
/**
* Method isLocalRecipient.
* @param recipient
* @return boolean
*/
protected boolean isLocalRecipient(MailAddress recipient)
{
return isLocalUser(recipient) && isLocalServer(recipient);
}
/**
* Method isLocalServer.
* @param recipient
* @return boolean
*/
protected boolean isLocalServer(MailAddress recipient)
{
return getServer().isLocalServer(recipient.getHost());
}
/**
* Method isLocalUser.
* @param recipient
* @return boolean
*/
protected boolean isLocalUser(MailAddress recipient)
{
return getLocalUsers().containsCaseInsensitive(recipient.getUser());
}
/**
* Method isBlacklistedRecipient.
* @param recipient
* @return boolean
*/
protected boolean isBlacklistedRecipient(MailAddress recipient)
{
return getBlacklist().contains(recipient);
}
/**
* Check if this mail has been bouncing by counting the X-fetched-from
* headers for this task
*
* @return boolean
*/
protected boolean isBouncing() throws MessagingException
{
Enumeration enumeration =
getMessageIn().getMatchingHeaderLines(
new String[] { "X-fetched-from" });
int count = 0;
while (enumeration.hasMoreElements())
{
String header = (String) enumeration.nextElement();
if (header.equals(getFetchTaskName()))
count++;
}
return count >= 3;
}
/**
* Method sendMail.
* @param mail
* @throws MessagingException
*/
protected void sendMail(Mail mail) throws MessagingException
{
// send the mail
getServer().sendMail(mail);
// Update the flags of the received message
if (!isLeave())
setMessageDeleted();
if (isMarkSeen())
setMessageSeen();
// Log the status
StringBuffer messageBuffer =
new StringBuffer("Spooled message to recipients: ");
Iterator recipientIterator = mail.getRecipients().iterator();
while (recipientIterator.hasNext())
{
messageBuffer.append(recipientIterator.next());
messageBuffer.append(' ');
}
messageBuffer.append('.');
logStatusInfo(messageBuffer.toString());
}
/**
* Method getEnvelopeRecipient answers the recipient if found else null.
*
* Try and parse the "for" parameter from a Received header
* Maybe not the most accurate parsing in the world but it should do
* I opted not to use ORO (maybe I should have)
*
* @param msg
* @return String
*/
protected String getEnvelopeRecipient(MimeMessage msg) throws MessagingException
{
String res = getCustomRecipientHeader();
if (res != null && res.length() > 0) {
String[] headers = msg.getHeader(getCustomRecipientHeader());
if (headers != null) {
String mailFor = headers[0];
if (mailFor.startsWith("<") && mailFor.endsWith(">"))
mailFor = mailFor.substring(1, (mailFor.length() - 1));
return mailFor;
}
} else {
try
{
Enumeration enumeration =
msg.getMatchingHeaderLines(new String[] { "Received" });
while (enumeration.hasMoreElements())
{
String received = (String) enumeration.nextElement();
int nextSearchAt = 0;
int i = 0;
int start = 0;
int end = 0;
boolean hasBracket = false;
boolean usableAddress = false;
while (!usableAddress && (i != -1))
{
hasBracket = false;
i = received.indexOf("for ", nextSearchAt);
if (i > 0)
{
start = i + 4;
end = 0;
nextSearchAt = start;
for (int c = start; c < received.length(); c++)
{
char ch = received.charAt(c);
switch (ch)
{
case '<' :
hasBracket = true;
continue;
case '@' :
usableAddress = true;
continue;
case ' ' :
end = c;
break;
case ';' :
end = c;
break;
}
if (end > 0)
break;
}
}
}
if (usableAddress)
{
// lets try and grab the email address
String mailFor = received.substring(start, end);
// strip the <> around the address if there are any
if (mailFor.startsWith("<") && mailFor.endsWith(">"))
mailFor = mailFor.substring(1, (mailFor.length() - 1));
return mailFor;
}
}
}
catch (MessagingException me)
{
logStatusWarn("No Received headers found.");
}
}
return null;
}
/**
* Method getIntendedRecipient answers the sole intended recipient else null.
*
* @return MailAddress
* @throws MessagingException
*/
protected MailAddress getIntendedRecipient() throws MessagingException
{
// If the original recipient should be ignored, answer the
// hard-coded recipient
if (isIgnoreRecipientHeader())
{
StringBuffer messageBuffer =
new StringBuffer("Ignoring recipient header. Using configured recipient as new envelope recipient: ");
messageBuffer.append(getRecipient());
messageBuffer.append('.');
logStatusInfo(messageBuffer.toString());
return getRecipient();
}
// If we can determine who the message was received for, answer
// the target recipient
String targetRecipient = getEnvelopeRecipient(getMessageIn());
if (targetRecipient != null)
{
MailAddress recipient = new MailAddress(targetRecipient);
StringBuffer messageBuffer =
new StringBuffer("Using original envelope recipient as new envelope recipient: ");
messageBuffer.append(recipient);
messageBuffer.append('.');
logStatusInfo(messageBuffer.toString());
return recipient;
}
// If we can determine the intended recipient from all of the recipients,
// answer the intended recipient. This requires that there is exactly one
// recipient answered by getAllRecipients(), which examines the TO: CC: and
// BCC: headers
Address[] allRecipients = getMessageIn().getAllRecipients();
if (allRecipients.length == 1)
{
MailAddress recipient =
new MailAddress((InternetAddress) allRecipients[0]);
StringBuffer messageBuffer =
new StringBuffer("Using sole recipient header address as new envelope recipient: ");
messageBuffer.append(recipient);
messageBuffer.append('.');
logStatusInfo(messageBuffer.toString());
return recipient;
}
return null;
}
/**
* Returns the messageIn.
* @return MimeMessage
*/
protected MimeMessage getMessageIn()
{
return fieldMessageIn;
}
/**
* Sets the messageIn.
* @param messageIn The messageIn to set
*/
protected void setMessageIn(MimeMessage messageIn)
{
fieldMessageIn = messageIn;
}
/**
* Returns the localRecipient.
* @return boolean
*/
protected boolean isRemoteRecipient()
{
return fieldRemoteRecipient;
}
/**
* Returns <code>boolean</code> indicating if the message to be delivered
* was unprocessed in a previous delivery attempt.
* @return boolean
*/
protected boolean isPreviouslyUnprocessed()
{
return true;
}
/**
* Log the status of the current message as INFO.
* @param detailMsg
*/
protected void logStatusInfo(String detailMsg) throws MessagingException
{
getLogger().info(getStatusReport(detailMsg).toString());
}
/**
* Log the status the current message as WARN.
* @param detailMsg
*/
protected void logStatusWarn(String detailMsg) throws MessagingException
{
getLogger().warn(getStatusReport(detailMsg).toString());
}
/**
* Log the status the current message as ERROR.
* @param detailMsg
*/
protected void logStatusError(String detailMsg) throws MessagingException
{
getLogger().error(getStatusReport(detailMsg).toString());
}
/**
* Answer a <code>StringBuffer</code> containing a message reflecting
* the current status of the message being processed.
*
* @param detailMsg
* @return StringBuffer
*/
protected StringBuffer getStatusReport(String detailMsg) throws MessagingException
{
StringBuffer messageBuffer = new StringBuffer(detailMsg);
if (detailMsg.length() > 0)
messageBuffer.append(' ');
messageBuffer.append("Message ID: ");
messageBuffer.append(getMessageIn().getMessageID());
messageBuffer.append(". Flags: Seen = ");
messageBuffer.append(new Boolean(isMessageSeen()));
messageBuffer.append(", Delete = ");
messageBuffer.append(new Boolean(isMessageDeleted()));
messageBuffer.append('.');
return messageBuffer;
}
/**
* Returns the userUndefined.
* @return boolean
*/
protected boolean isUserUndefined()
{
return fieldUserUndefined;
}
/**
* Is the DELETED flag set?
* @throws MessagingException
*/
protected boolean isMessageDeleted() throws MessagingException
{
return getMessageIn().isSet(Flags.Flag.DELETED);
}
/**
* Is the SEEN flag set?
* @throws MessagingException
*/
protected boolean isMessageSeen() throws MessagingException
{
return getMessageIn().isSet(Flags.Flag.SEEN);
}
/**
* Set the DELETED flag.
* @throws MessagingException
*/
protected void setMessageDeleted() throws MessagingException
{
getMessageIn().setFlag(Flags.Flag.DELETED, true);
}
/* /**
* Set the SEEN flag.
* @throws MessagingException
*/
protected void setMessageSeen() throws MessagingException
{
// If the Seen flag is not handled by the folder
// allow a handler to do whatever it deems necessary
if (!getMessageIn()
.getFolder()
.getPermanentFlags()
.contains(Flags.Flag.SEEN))
handleMarkSeenNotPermanent();
else
getMessageIn().setFlag(Flags.Flag.SEEN, true);
}
/**
* <p>Handler for when the folder does not support the SEEN flag.
* The default behaviour implemented here is to log a warning and set the
* flag anyway.</p>
*
* <p> Subclasses may choose to override this and implement their own
* solutions.</p>
*
* @throws MessagingException
*/
protected void handleMarkSeenNotPermanent() throws MessagingException
{
getMessageIn().setFlag(Flags.Flag.SEEN, true);
logStatusWarn("Message marked as SEEN, but the folder does not support a permanent SEEN flag.");
}
/**
* Returns the Blacklisted.
* @return boolean
*/
protected boolean isBlacklistedRecipient()
{
return fieldBlacklistedRecipient;
}
/**
* Sets the localRecipient.
* @param localRecipient The localRecipient to set
*/
protected void setRemoteRecipient(boolean localRecipient)
{
fieldRemoteRecipient = localRecipient;
}
/**
* Sets the userUndefined.
* @param userUndefined The userUndefined to set
*/
protected void setUserUndefined(boolean userUndefined)
{
fieldUserUndefined = userUndefined;
}
/**
* Adds the mail attributes to a <code>Mail</code>.
* @param aMail a Mail instance
*/
protected void addMailAttributes(Mail aMail) throws MessagingException
{
aMail.setAttribute(
getAttributePrefix() + "taskName",
getFetchTaskName());
aMail.setAttribute(
getAttributePrefix() + "folderName",
getMessageIn().getFolder().getFullName());
if (isRemoteRecipient())
aMail.setAttribute(
getAttributePrefix() + "isRemoteRecipient",
null);
if (isUserUndefined())
aMail.setAttribute(getAttributePrefix() + "isUserUndefined", null);
if (isBlacklistedRecipient())
aMail.setAttribute(
getAttributePrefix() + "isBlacklistedRecipient",
null);
if (isRecipientNotFound())
aMail.setAttribute(
getAttributePrefix() + "isRecipientNotFound",
null);
if (isMaxMessageSizeExceeded().booleanValue())
aMail.setAttribute(
getAttributePrefix() + "isMaxMessageSizeExceeded",
new Integer(getMessageIn().getSize()).toString());
if (isRemoteReceivedHeaderInvalid().booleanValue())
aMail.setAttribute(
getAttributePrefix() + "isRemoteReceivedHeaderInvalid",
null);
if (isDefaultSenderLocalPart())
aMail.setAttribute(
getAttributePrefix() + "isDefaultSenderLocalPart",
null);
if (isDefaultSenderDomainPart())
aMail.setAttribute(
getAttributePrefix() + "isDefaultSenderDomainPart",
null);
if (isDefaultRemoteAddress())
aMail.setAttribute(
getAttributePrefix() + "isDefaultRemoteAddress",
null);
}
/**
* Adds any required error messages to a <code>Mail</code>.
* @param aMail a Mail instance
*/
protected void addErrorMessages(Mail mail) throws MessagingException
{
if (isMaxMessageSizeExceeded().booleanValue())
{
StringBuffer msgBuffer =
new StringBuffer("550 - Rejected - This message has been rejected as the message size of ");
msgBuffer.append(getMessageIn().getSize() * 1000 / 1024 / 1000f);
msgBuffer.append("KB exceeds the maximum permitted size of ");
msgBuffer.append(getMaxMessageSizeLimit() / 1024);
msgBuffer.append("KB.");
mail.setErrorMessage(msgBuffer.toString());
}
}
/**
* Sets the Blacklisted.
* @param blacklisted The blacklisted to set
*/
protected void setBlacklistedRecipient(boolean blacklisted)
{
fieldBlacklistedRecipient = blacklisted;
}
/**
* Returns the recipientNotFound.
* @return boolean
*/
protected boolean isRecipientNotFound()
{
return fieldRecipientNotFound;
}
/**
* Sets the recipientNotFound.
* @param recipientNotFound The recipientNotFound to set
*/
protected void setRecipientNotFound(boolean recipientNotFound)
{
fieldRecipientNotFound = recipientNotFound;
}
/**
* Returns the remoteDomain, lazily initialised as required.
* @return String
*/
protected String getRemoteDomain() throws MessagingException
{
String remoteDomain;
if (null == (remoteDomain = getRemoteDomainBasic()))
{
updateRemoteDomain();
return getRemoteDomain();
}
return remoteDomain;
}
/**
* Returns the remoteDomain.
* @return String
*/
private String getRemoteDomainBasic()
{
return fieldRemoteDomain;
}
/**
* Sets the remoteDomain.
* @param remoteDomain The remoteDomain to set
*/
protected void setRemoteDomain(String remoteDomain)
{
fieldRemoteDomain = remoteDomain;
}
/**
* Updates the remoteDomain.
*/
protected void updateRemoteDomain() throws MessagingException
{
setRemoteDomain(computeRemoteDomain());
}
/**
* Answer the IP Address of the remote server for the message being
* processed.
* @return String
* @throws MessagingException
* @throws UnknownHostException
*/
protected String computeRemoteAddress()
throws MessagingException, UnknownHostException
{
String domain = getRemoteDomain();
String address = null;
String validatedAddress = null;
int ipAddressStart = domain.indexOf('[');
int ipAddressEnd = -1;
if (ipAddressStart > -1)
ipAddressEnd = domain.indexOf(']', ipAddressStart);
if (ipAddressEnd > -1)
address = domain.substring(ipAddressStart + 1, ipAddressEnd);
else
{
int hostNameEnd = domain.indexOf(' ');
if (hostNameEnd == -1)
hostNameEnd = domain.length();
address = domain.substring(0, hostNameEnd);
}
validatedAddress = org.apache.james.dnsserver.DNSServer.getByName(address).getHostAddress();
return validatedAddress;
}
/**
* Answer the Canonical host name of the remote server for the message
* being processed.
* @return String
* @throws MessagingException
* @throws UnknownHostException
*/
protected String computeRemoteHostName()
throws MessagingException, UnknownHostException
{
// These shenanigans are required to get the fully qualified
// hostname prior to JDK 1.4 in which get getCanonicalHostName()
// does the job for us
InetAddress addr1 = org.apache.james.dnsserver.DNSServer.getByName(getRemoteAddress());
InetAddress addr2 = org.apache.james.dnsserver.DNSServer.getByName(addr1.getHostAddress());
return addr2.getHostName();
}
/**
* Returns the remoteAddress, lazily initialised as required.
* @return String
*/
protected String getRemoteAddress()
throws MessagingException, UnknownHostException
{
String remoteAddress;
if (null == (remoteAddress = getRemoteAddressBasic()))
{
updateRemoteAddress();
return getRemoteAddress();
}
return remoteAddress;
}
/**
* Returns the remoteAddress.
* @return String
*/
private String getRemoteAddressBasic()
{
return fieldRemoteAddress;
}
/**
* Returns the remoteHostName, lazily initialised as required.
* @return String
*/
protected String getRemoteHostName()
throws MessagingException, UnknownHostException
{
String remoteHostName;
if (null == (remoteHostName = getRemoteHostNameBasic()))
{
updateRemoteHostName();
return getRemoteHostName();
}
return remoteHostName;
}
/**
* Returns the remoteHostName.
* @return String
*/
private String getRemoteHostNameBasic()
{
return fieldRemoteHostName;
}
/**
* Sets the remoteAddress.
* @param remoteAddress The remoteAddress to set
*/
protected void setRemoteAddress(String remoteAddress)
{
fieldRemoteAddress = remoteAddress;
}
/**
* Updates the remoteAddress.
*/
protected void updateRemoteAddress()
throws MessagingException, UnknownHostException
{
setRemoteAddress(computeRemoteAddress());
}
/**
* Sets the remoteHostName.
* @param remoteHostName The remoteHostName to set
*/
protected void setRemoteHostName(String remoteHostName)
{
fieldRemoteHostName = remoteHostName;
}
/**
* Updates the remoteHostName.
*/
protected void updateRemoteHostName()
throws MessagingException, UnknownHostException
{
setRemoteHostName(computeRemoteHostName());
}
/**
* Returns the rFC2822RECEIVEDHeaderFields.
* @return String
*/
public static String getRFC2822RECEIVEDHeaderFields()
{
return fieldRFC2822RECEIVEDHeaderFields;
}
/**
* Returns the maxMessageSizeExceeded, lazily initialised as required.
* @return Boolean
*/
protected Boolean isMaxMessageSizeExceeded() throws MessagingException
{
Boolean isMaxMessageSizeExceeded = null;
if (null
== (isMaxMessageSizeExceeded = isMaxMessageSizeExceededBasic()))
{
updateMaxMessageSizeExceeded();
return isMaxMessageSizeExceeded();
}
return isMaxMessageSizeExceeded;
}
/**
* Refreshes the maxMessageSizeExceeded.
*/
protected void updateMaxMessageSizeExceeded() throws MessagingException
{
setMaxMessageSizeExceeded(computeMaxMessageSizeExceeded());
}
/**
* Compute the maxMessageSizeExceeded.
* @return Boolean
*/
protected Boolean computeMaxMessageSizeExceeded() throws MessagingException
{
if (0 == getMaxMessageSizeLimit())
return Boolean.FALSE;
return new Boolean(getMessageIn().getSize() > getMaxMessageSizeLimit());
}
/**
* Returns the maxMessageSizeExceeded.
* @return Boolean
*/
private Boolean isMaxMessageSizeExceededBasic()
{
return fieldMaxMessageSizeExceeded;
}
/**
* Sets the maxMessageSizeExceeded.
* @param maxMessageSizeExceeded The maxMessageSizeExceeded to set
*/
protected void setMaxMessageSizeExceeded(Boolean maxMessageSizeExceeded)
{
fieldMaxMessageSizeExceeded = maxMessageSizeExceeded;
}
/**
* Returns the remoteReceivedHeaderInvalid, lazily initialised.
* @return Boolean
*/
protected Boolean isRemoteReceivedHeaderInvalid() throws MessagingException
{
Boolean isInvalid = null;
if (null == (isInvalid = isRemoteReceivedHeaderInvalidBasic()))
{
updateRemoteReceivedHeaderInvalid();
return isRemoteReceivedHeaderInvalid();
}
return isInvalid;
}
/**
* Computes the remoteReceivedHeaderInvalid.
* @return Boolean
*/
protected Boolean computeRemoteReceivedHeaderInvalid()
throws MessagingException
{
Boolean isInvalid = Boolean.FALSE;
try
{
getRemoteAddress();
}
catch (UnknownHostException e)
{
isInvalid = Boolean.TRUE;
}
return isInvalid;
}
/**
* Returns the remoteReceivedHeaderInvalid.
* @return Boolean
*/
private Boolean isRemoteReceivedHeaderInvalidBasic()
{
return fieldRemoteReceivedHeaderInvalid;
}
/**
* Sets the remoteReceivedHeaderInvalid.
* @param remoteReceivedHeaderInvalid The remoteReceivedHeaderInvalid to set
*/
protected void setRemoteReceivedHeaderInvalid(Boolean remoteReceivedHeaderInvalid)
{
fieldRemoteReceivedHeaderInvalid = remoteReceivedHeaderInvalid;
}
/**
* Updates the remoteReceivedHeaderInvalid.
*/
protected void updateRemoteReceivedHeaderInvalid() throws MessagingException
{
setRemoteReceivedHeaderInvalid(computeRemoteReceivedHeaderInvalid());
}
/**
* Returns the defaultSenderDomainPart.
* @return boolean
*/
protected boolean isDefaultSenderDomainPart()
{
return fieldDefaultSenderDomainPart;
}
/**
* Returns the defaultSenderLocalPart.
* @return boolean
*/
protected boolean isDefaultSenderLocalPart()
{
return fieldDefaultSenderLocalPart;
}
/**
* Sets the defaultSenderDomainPart.
* @param defaultSenderDomainPart The defaultSenderDomainPart to set
*/
protected void setDefaultSenderDomainPart(boolean defaultSenderDomainPart)
{
fieldDefaultSenderDomainPart = defaultSenderDomainPart;
}
/**
* Sets the defaultSenderLocalPart.
* @param defaultSenderLocalPart The defaultSenderLocalPart to set
*/
protected void setDefaultSenderLocalPart(boolean defaultSenderLocalPart)
{
fieldDefaultSenderLocalPart = defaultSenderLocalPart;
}
/**
* Returns the defaultRemoteAddress.
* @return boolean
*/
protected boolean isDefaultRemoteAddress()
{
return fieldDefaultRemoteAddress;
}
/**
* Sets the defaultRemoteAddress.
* @param defaultRemoteAddress The defaultRemoteAddress to set
*/
protected void setDefaultRemoteAddress(boolean defaultRemoteAddress)
{
fieldDefaultRemoteAddress = defaultRemoteAddress;
}
}
|