/****************************************************************
* 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.mailetcontainer.impl;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.HierarchicalConfiguration;
import org.apache.james.core.MailImpl;
import org.apache.james.dnsservice.api.DNSService;
import org.apache.james.dnsservice.api.TemporaryResolutionException;
import org.apache.james.dnsservice.library.MXHostAddressIterator;
import org.apache.james.domainlist.api.DomainList;
import org.apache.james.domainlist.api.DomainListException;
import org.apache.james.lifecycle.api.Configurable;
import org.apache.james.lifecycle.api.LifecycleUtil;
import org.apache.james.lifecycle.api.LogEnabled;
import org.apache.james.mailetcontainer.api.MailProcessor;
import org.apache.james.user.api.UsersRepository;
import org.apache.james.user.api.UsersRepositoryException;
import org.apache.mailet.HostAddress;
import org.apache.mailet.LookupException;
import org.apache.mailet.Mail;
import org.apache.mailet.MailAddress;
import org.apache.mailet.MailetContext;
import org.apache.mailet.base.RFC2822Headers;
import org.slf4j.Logger;
import javax.inject.Inject;
import javax.mail.Address;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Vector;
public class JamesMailetContext implements MailetContext, LogEnabled, Configurable {
/**
* A hash table of server attributes These are the MailetContext attributes
*/
private final Hashtable<String, Object> attributes = new Hashtable<String, Object>();
protected DNSService dns;
protected Logger log;
private UsersRepository localusers;
private MailProcessor processorList;
private DomainList domains;
private MailAddress postmaster;
@Inject
public void setMailProcessor(MailProcessor processorList) {
this.processorList = processorList;
}
@Inject
public void setDNSService(DNSService dns) {
this.dns = dns;
}
@Inject
public void setUsersRepository(UsersRepository localusers) {
this.localusers = localusers;
}
@Inject
public void setDomainList(DomainList domains) {
this.domains = domains;
}
@Override
public Collection<String> getMailServers(String host) {
try {
return dns.findMXRecords(host);
} catch (TemporaryResolutionException e) {
// TODO: We only do this to not break backward compatiblity. Should
// fixed later
return Collections.unmodifiableCollection(new ArrayList<String>(0));
}
}
@Override
public Object getAttribute(String key) {
return attributes.get(key);
}
@Override
public void setAttribute(String key, Object object) {
attributes.put(key, object);
}
@Override
public void removeAttribute(String key) {
attributes.remove(key);
}
@Override
public Iterator<String> getAttributeNames() {
Vector<String> names = new Vector<String>();
for (Enumeration<String> e = attributes.keys(); e.hasMoreElements(); ) {
names.add(e.nextElement());
}
return names.iterator();
}
/**
* This generates a response to the Return-Path address, or the address of
* the message's sender if the Return-Path is not available. Note that this
* is different than a mail-client's reply, which would use the Reply-To or
* From header. This will send the bounce with the server's postmaster as
* the sender.
*
* @see org.apache.mailet.MailetContext#bounce(Mail, String)
*/
@Override
public void bounce(Mail mail, String message) throws MessagingException {
bounce(mail, message, getPostmaster());
}
/**
* <p>
* This generates a response to the Return-Path address, or the address of
* the message's sender if the Return-Path is not available. Note that this
* is different than a mail-client's reply, which would use the Reply-To or
* From header.
* </p>
* <p>
* Bounced messages are attached in their entirety (headers and content) and
* the resulting MIME part type is "message/rfc822".
* </p>
* <p>
* The attachment to the subject of the original message (or "No Subject" if
* there is no subject in the original message)
* </p>
* <p>
* There are outstanding issues with this implementation revolving around
* handling of the return-path header.
* </p>
* <p>
* MIME layout of the bounce message:
* </p>
* <p>
* multipart (mixed)/ contentPartRoot (body) = mpContent (alternative)/ part
* (body) = message part (body) = original
* </p>
*
* @see org.apache.mailet.MailetContext#bounce(Mail, String, MailAddress)
*/
@Override
public void bounce(Mail mail, String message, MailAddress bouncer) throws MessagingException {
if (mail.getSender() == null) {
if (log.isInfoEnabled())
log.info("Mail to be bounced contains a null (<>) reverse path. No bounce will be sent.");
return;
} else {
// Bounce message goes to the reverse path, not to the Reply-To
// address
if (log.isInfoEnabled())
log.info("Processing a bounce request for a message with a reverse path of " + mail.getSender().toString());
}
MailImpl reply = rawBounce(mail, message);
// Change the sender...
reply.getMessage().setFrom(bouncer.toInternetAddress());
reply.getMessage().saveChanges();
// Send it off ... with null reverse-path
reply.setSender(null);
sendMail(reply);
LifecycleUtil.dispose(reply);
}
@Override
public List<String> dnsLookup(String s, RecordType recordType) throws LookupException {
throw new UnsupportedOperationException("Not implemented yet!");
}
/**
* Generates a bounce mail that is a bounce of the original message.
*
* @param bounceText the text to be prepended to the message to describe the bounce
* condition
* @return the bounce mail
* @throws MessagingException if the bounce mail could not be created
*/
private MailImpl rawBounce(Mail mail, String bounceText) throws MessagingException {
// This sends a message to the james component that is a bounce of the
// sent message
MimeMessage original = mail.getMessage();
MimeMessage reply = (MimeMessage) original.reply(false);
reply.setSubject("Re: " + original.getSubject());
reply.setSentDate(new Date());
Collection<MailAddress> recipients = new HashSet<MailAddress>();
recipients.add(mail.getSender());
InternetAddress addr[] = {new InternetAddress(mail.getSender().toString())};
reply.setRecipients(Message.RecipientType.TO, addr);
reply.setFrom(new InternetAddress(mail.getRecipients().iterator().next().toString()));
reply.setText(bounceText);
reply.setHeader(RFC2822Headers.MESSAGE_ID, "replyTo-" + mail.getName());
return new MailImpl("replyTo-" + mail.getName(), new MailAddress(mail.getRecipients().iterator().next().toString()), recipients, reply);
}
@Override
public boolean isLocalUser(String name) {
if (name == null) {
return false;
}
try {
if (!name.contains("@")) {
try {
return isLocalEmail(new MailAddress(name.toLowerCase(), domains.getDefaultDomain()));
} catch (DomainListException e) {
log("Unable to access DomainList", e);
return false;
}
} else {
return isLocalEmail(new MailAddress(name.toLowerCase()));
}
} catch (ParseException e) {
log("Error checking isLocalUser for user " + name);
return false;
}
}
@Override
public boolean isLocalEmail(MailAddress mailAddress) {
if (mailAddress != null) {
String userName = mailAddress.toString().toLowerCase();
if (!isLocalServer(mailAddress.getDomain().toLowerCase())) {
return false;
}
try {
if (!localusers.supportVirtualHosting()) {
userName = mailAddress.getLocalPart().toLowerCase();
}
return localusers.contains(userName);
} catch (UsersRepositoryException e) {
log("Unable to access UsersRepository", e);
}
}
return false;
}
@Override
public MailAddress getPostmaster() {
return postmaster;
}
@Override
public int getMajorVersion() {
return 2;
}
@Override
public int getMinorVersion() {
return 4;
}
/**
* Performs DNS lookups as needed to find servers which should or might
* support SMTP. Returns an Iterator over HostAddress, a specialized
* subclass of javax.mail.URLName, which provides location information for
* servers that are specified as mail handlers for the given hostname. This
* is done using MX records, and the HostAddress instances are returned
* sorted by MX priority. If no host is found for domainName, the Iterator
* returned will be empty and the first call to hasNext() will return false.
*
* @param domainName - the domain for which to find mail servers
* @return an Iterator over HostAddress instances, sorted by priority
* @see org.apache.james.dnsservice.api.DNSService#getHostName(java.net.InetAddress)
* @since Mailet API v2.2.0a16-unstable
*/
@Override
public Iterator<HostAddress> getSMTPHostAddresses(String domainName) {
try {
return new MXHostAddressIterator(dns.findMXRecords(domainName).iterator(), dns, false, log);
} catch (TemporaryResolutionException e) {
// TODO: We only do this to not break backward compatiblity. Should
// fixed later
return Collections.unmodifiableCollection(new ArrayList<HostAddress>(0)).iterator();
}
}
@Override
public String getServerInfo() {
return "Apache JAMES";
}
@Override
public boolean isLocalServer(String name) {
try {
return domains.containsDomain(name);
} catch (DomainListException e) {
log.error("Unable to retrieve domains", e);
return false;
}
}
@Override
@Deprecated
public void log(String arg0) {
log.info(arg0);
}
@Override
@Deprecated
public void log(String arg0, Throwable arg1) {
log.info(arg0, arg1);
}
@Override
public void log(LogLevel logLevel, String s) {
switch (logLevel) {
case INFO:
log.info(s);
break;
case WARN:
log.warn(s);
break;
case ERROR:
log.error(s);
break;
default:
log.debug(s);
}
}
@Override
public void log(LogLevel logLevel, String s, Throwable throwable) {
switch (logLevel) {
case INFO:
log.info(s, throwable);
break;
case WARN:
log.warn(s, throwable);
break;
case ERROR:
log.error(s, throwable);
break;
default:
log.debug(s, throwable);
}
}
/**
* Place a mail on the spool for processing
*
* @param message the message to send
* @throws MessagingException if an exception is caught while placing the mail on the spool
*/
@Override
public void sendMail(MimeMessage message) throws MessagingException {
MailAddress sender = new MailAddress((InternetAddress) message.getFrom()[0]);
Collection<MailAddress> recipients = new HashSet<MailAddress>();
Address addresses[] = message.getAllRecipients();
if (addresses != null) {
for (Address address : addresses) {
// Javamail treats the "newsgroups:" header field as a
// recipient, so we want to filter those out.
if (address instanceof InternetAddress) {
recipients.add(new MailAddress((InternetAddress) address));
}
}
}
sendMail(sender, recipients, message);
}
@SuppressWarnings("unchecked")
public void sendMail(MailAddress sender, Collection recipients, MimeMessage message) throws MessagingException {
sendMail(sender, recipients, message, Mail.DEFAULT);
}
/**
* TODO: Should we use the MailProcessorList or the MailQueue here ?
*/
@Override
public void sendMail(Mail mail) throws MessagingException {
processorList.service(mail);
}
@SuppressWarnings("unchecked")
public void sendMail(MailAddress sender, Collection recipients, MimeMessage message, String state) throws MessagingException {
MailImpl mail = new MailImpl(MailImpl.getId(), sender, recipients, message);
try {
mail.setState(state);
sendMail(mail);
} finally {
LifecycleUtil.dispose(mail);
}
}
/**
* <p>
* This method has been moved to LocalDelivery (the only client of the
* method). Now we can safely remove it from the Mailet API and from this
* implementation of MailetContext.
* </p>
* <p>
* The local field localDeliveryMailet will be removed when we remove the
* storeMail method.
* </p>
*
* @deprecated since 2.2.0 look at the LocalDelivery code to find out how to
* do the local delivery.
*/
public void storeMail(MailAddress sender, MailAddress recipient, MimeMessage msg) {
throw new UnsupportedOperationException("Was removed");
}
@Override
public void setLog(Logger log) {
this.log = log;
}
@Override
public void configure(HierarchicalConfiguration config) throws ConfigurationException {
try {
// Get postmaster
String postMasterAddress = config.getString("postmaster", "postmaster").toLowerCase(Locale.US);
// if there is no @domain part, then add the first one from the
// list of supported domains that isn't localhost. If that
// doesn't work, use the hostname, even if it is localhost.
if (postMasterAddress.indexOf('@') < 0) {
String domainName = null; // the domain to use
// loop through candidate domains until we find one or exhaust
// the
// list
String[] doms = domains.getDomains();
if (doms != null) {
for (String dom : doms) {
String serverName = dom.toLowerCase(Locale.US);
if (!("localhost".equals(serverName))) {
domainName = serverName; // ok, not localhost, so
// use it
}
}
}
// if we found a suitable domain, use it. Otherwise fallback to
// the
// host name.
postMasterAddress = postMasterAddress + "@" + (domainName != null ? domainName : domains.getDefaultDomain());
}
try {
this.postmaster = new MailAddress(postMasterAddress);
if (!domains.containsDomain(postmaster.getDomain())) {
String warnBuffer = "The specified postmaster address ( " + postmaster + " ) is not a local " +
"address. This is not necessarily a problem, but it does mean that emails addressed to " +
"the postmaster will be routed to another server. For some configurations this may " +
"cause problems.";
log.warn(warnBuffer);
}
} catch (AddressException e) {
throw new ConfigurationException("Postmaster address " + postMasterAddress + "is invalid", e);
}
} catch (DomainListException e) {
throw new ConfigurationException("Unable to access DomainList", e);
}
}
}