FileDocCategorySizeDatePackage
SMIMEAbstractSign.javaAPI DocApache James 2.3.128289Fri Jan 12 12:56:30 GMT 2007org.apache.james.transport.mailets.smime

SMIMEAbstractSign

public abstract class SMIMEAbstractSign extends org.apache.mailet.GenericMailet

Abstract mailet providing common SMIME signature services.
It can be subclassed to make authoring signing mailets simple.
By extending it and overriding one or more of the following methods a new behaviour can be quickly created without the author having to address any issue other than the relevant one:

  • {@link #initDebug}, {@link #setDebug} and {@link #isDebug} manage the debugging mode.
  • {@link #initExplanationText}, {@link #setExplanationText} and {@link #getExplanationText} manage the text of an attachment that will be added to explain the meaning of this server-side signature.
  • {@link #initKeyHolder}, {@link #setKeyHolder} and {@link #getKeyHolder} manage the {@link KeyHolder} object that will contain the keys and certificates and will do the crypto work.
  • {@link #initPostmasterSigns}, {@link #setPostmasterSigns} and {@link #isPostmasterSigns} determines whether messages originated by the Postmaster will be signed or not.
  • {@link #initRebuildFrom}, {@link #setRebuildFrom} and {@link #isRebuildFrom} determines whether the "From:" header will be rebuilt to neutralize the wrong behaviour of some MUAs like Microsoft Outlook Express.
  • {@link #initSignerName}, {@link #setSignerName} and {@link #getSignerName} manage the name of the signer to be shown in the explanation text.
  • {@link #isOkToSign} controls whether the mail can be signed or not.
  • The abstract method {@link #getWrapperBodyPart} returns the massaged {@link javax.mail.internet.MimeBodyPart} that will be signed, or null if the message has to be signed "as is".

Handles the following init parameters:

  • <debug>: if true some useful information is logged. The default is false.
  • <keyStoreFileName>: the {@link java.security.KeyStore} full file name.
  • <keyStorePassword>: the KeyStore password. If given, it is used to check the integrity of the keystore data, otherwise, if null, the integrity of the keystore is not checked.
  • <keyAlias>: the alias name to use to search the Key using {@link java.security.KeyStore#getKey}. The default is to look for the first and only alias in the keystore; if zero or more than one is found a {@link java.security.KeyStoreException} is thrown.
  • <keyAliasPassword>: the alias password. The default is to use the KeyStore password. At least one of the passwords must be provided.
  • <keyStoreType>: the type of the keystore. The default will use {@link java.security.KeyStore#getDefaultType}.
  • <postmasterSigns>: if true the message will be signed even if the sender is the Postmaster. The default is false.
  • <rebuildFrom>: If true will modify the "From:" header. For more info see {@link #isRebuildFrom}. The default is false.
  • <signerName>: the name of the signer to be shown in the explanation text. The default is to use the "CN=" property of the signing certificate.
  • <explanationText>: the text of an explanation of the meaning of this server-side signature. May contain the following substitution patterns (see also {@link #getReplacedExplanationText}): [signerName], [signerAddress], [reversePath], [headers]. It should be included in the signature. The actual presentation of the text depends on the specific concrete mailet subclass: see for example {@link SMIMESign}. The default is to not have any explanation text.
version
CVS $Revision: 494012 $ $Date: 2007-01-08 11:23:58 +0100 (Mo, 08 Jan 2007) $
since
2.2.1

Fields Summary
private static final String
HEADERS_PATTERN
private static final String
SIGNER_NAME_PATTERN
private static final String
SIGNER_ADDRESS_PATTERN
private static final String
REVERSE_PATH_PATTERN
private boolean
debug
Holds value of property debug.
private String
explanationText
Holds value of property explanationText.
private org.apache.james.security.KeyHolder
keyHolder
Holds value of property keyHolder.
private boolean
postmasterSigns
Holds value of property postmasterSigns.
private boolean
rebuildFrom
Holds value of property rebuildFrom.
private String
signerName
Holds value of property signerName.
Constructors Summary
Methods Summary
private final java.lang.StringarrayToString(java.lang.Object[] array)
Utility method for obtaining a string representation of an array of Objects.

        if (array == null) {
            return "null";
        }
        StringBuffer sb = new StringBuffer(1024);
        sb.append("[");
        for (int i = 0; i < array.length; i++) {
            if (i > 0) {
                sb.append(",");
            }
            sb.append(array[i]);
        }
        sb.append("]");
        return sb.toString();
    
private voidcheckInitParameters(java.lang.String[] allowedArray)
Checks if there are unallowed init parameters specified in the configuration file against the String[] allowedInitParameters.

        // if null then no check is requested
        if (allowedArray == null) {
            return;
        }
        
        Collection allowed = new HashSet();
        Collection bad = new ArrayList();
        
        for (int i = 0; i < allowedArray.length; i++) {
            allowed.add(allowedArray[i]);
        }
        
        Iterator iterator = getInitParameterNames();
        while (iterator.hasNext()) {
            String parameter = (String) iterator.next();
            if (!allowed.contains(parameter)) {
                bad.add(parameter);
            }
        }
        
        if (bad.size() > 0) {
            throw new MessagingException("Unexpected init parameters found: "
            + arrayToString(bad.toArray()));
        }
    
protected final booleanfromAddressSameAsReverse(org.apache.mailet.Mail mail)
Utility method that checks if there is at least one address in the "From:" header same as the reverse-path.

param
mail The mail to check.
return
True if an address is found, false otherwise.

        
        MailAddress reversePath = mail.getSender();
        
        if (reversePath == null) {
            return false;
        }
        
        try {
            InternetAddress[] fromArray = (InternetAddress[]) mail.getMessage().getFrom();
            if (fromArray != null) {
                for (int i = 0; i < fromArray.length; i++) {
                    MailAddress mailAddress  = null;
                    try {
                        mailAddress = new MailAddress(fromArray[i]);
                    } catch (ParseException pe) {
                        log("Unable to parse a \"FROM\" header address: " + fromArray[i].toString() + "; ignoring.");
                        continue;
                    }
                    if (mailAddress.equals(reversePath)) {
                        return true;
                    }
                }
            }
        } catch (MessagingException me) {
            log("Unable to parse the \"FROM\" header; ignoring.");
        }
        
        return false;
        
    
protected abstract java.lang.String[]getAllowedInitParameters()
Gets the expected init parameters.

return
An array containing the parameter names allowed for this mailet.

public java.lang.StringgetExplanationText()
Getter for property explanationText. Text to be used in the SignatureExplanation.txt file.

return
Value of property explanationText.

        return this.explanationText;
    
protected org.apache.james.security.KeyHoldergetKeyHolder()
Getter for property keyHolder. It is protected instead of public for security reasons.

return
Value of property keyHolder.

        return this.keyHolder;
    
protected final java.lang.StringgetMessageHeaders(javax.mail.internet.MimeMessage message)
Utility method for obtaining a string representation of the Message's headers

param
message The message to extract the headers from.
return
The string containing the headers.

        Enumeration heads = message.getAllHeaderLines();
        StringBuffer headBuffer = new StringBuffer(1024);
        while(heads.hasMoreElements()) {
            headBuffer.append(heads.nextElement().toString()).append("\r\n");
        }
        return headBuffer.toString();
    
protected final java.lang.StringgetReplacedExplanationText(java.lang.String explanationText, java.lang.String signerName, java.lang.String signerAddress, java.lang.String reversePath, java.lang.String headers)
Prepares the explanation text making substitutions in the explanationText template string. Utility method that searches for all occurrences of some pattern strings and substitute them with the appropriate params.

param
explanationText The template string for the explanation text.
param
signerName The string that will replace the [signerName] pattern.
param
signerAddress The string that will replace the [signerAddress] pattern.
param
reversePath The string that will replace the [reversePath] pattern.
param
headers The string that will replace the [headers] pattern.
return
The actual explanation text string with all replacements done.

        
        String replacedExplanationText = explanationText;
        
        replacedExplanationText = getReplacedString(replacedExplanationText, SIGNER_NAME_PATTERN, signerName);
        replacedExplanationText = getReplacedString(replacedExplanationText, SIGNER_ADDRESS_PATTERN, signerAddress);
        replacedExplanationText = getReplacedString(replacedExplanationText, REVERSE_PATH_PATTERN, reversePath);
        replacedExplanationText = getReplacedString(replacedExplanationText, HEADERS_PATTERN, headers);
        
        return replacedExplanationText;
    
private java.lang.StringgetReplacedString(java.lang.String template, java.lang.String pattern, java.lang.String actual)
Searches the template String for all occurrences of the pattern string and creates a new String substituting them with the actual String.

param
template The template String to work on.
param
pattern The string to search for the replacement.
param
actual The actual string to use for the replacement.

         if (actual != null) {
             StringBuffer sb = new StringBuffer(template.length());
            int fromIndex = 0;
            int index;
            while ((index = template.indexOf(pattern, fromIndex)) >= 0) {
                sb.append(template.substring(fromIndex, index));
                sb.append(actual);
                fromIndex = index + pattern.length();
            }
            if (fromIndex < template.length()){
                sb.append(template.substring(fromIndex));
            }
            return sb.toString();
        } else {
            return new String(template);
        }
    
public java.lang.StringgetSignerName()
Getter for property signerName.

return
Value of property signerName.

        return this.signerName;
    
protected abstract javax.mail.internet.MimeBodyPartgetWrapperBodyPart(org.apache.mailet.Mail mail)
Creates the {@link javax.mail.internet.MimeBodyPart} that will be signed. For example, may attach a text file explaining the meaning of the signature, or an XML file containing information that can be checked by other MTAs.

param
mail The mail to massage.
return
The massaged MimeBodyPart to sign, or null to have the whole message signed "as is".

public voidinit()
Mailet initialization routine.

        
        // check that all init parameters have been declared in allowedInitParameters
        checkInitParameters(getAllowedInitParameters());
        
        try {
            initDebug();
            if (isDebug()) {
                log("Initializing");
            }
            
            initKeyHolder();
            initSignerName();
            initPostmasterSigns();
            initRebuildFrom();
            initExplanationText();
            
            
        } catch (MessagingException me) {
            throw me;
        } catch (Exception e) {
            log("Exception thrown", e);
            throw new MessagingException("Exception thrown", e);
        } finally {
            if (isDebug()) {
                StringBuffer logBuffer =
                new StringBuffer(1024)
                .append("Other parameters:")
                .append(", signerName=").append(getSignerName())
                .append(", postmasterSigns=").append(postmasterSigns)
                .append(", rebuildFrom=").append(rebuildFrom)
                .append(" ");
                log(logBuffer.toString());
            }
        }
        
    
protected voidinitDebug()
Initializer for property debug.

    
                         
       
    
    /* ******************************************************************** */
    /* ****************** Begin of setters and getters ******************** */
    /* ******************************************************************** */
    
             
       
        setDebug((getInitParameter("debug") == null) ? false : new Boolean(getInitParameter("debug")).booleanValue());
    
protected voidinitExplanationText()
Initializer for property explanationText.

        setExplanationText(getInitParameter("explanationText"));
        if (isDebug()) {
            log("Explanation text:\r\n" + getExplanationText());
        }
    
protected voidinitKeyHolder()
Initializer for property keyHolder.

        String keyStoreFileName = getInitParameter("keyStoreFileName");
        if (keyStoreFileName == null) {
            throw new MessagingException("<keyStoreFileName> parameter missing.");
        }
        
        String keyStorePassword = getInitParameter("keyStorePassword");
        if (keyStorePassword == null) {
            throw new MessagingException("<keyStorePassword> parameter missing.");
        }
        String keyAliasPassword = getInitParameter("keyAliasPassword");
        if (keyAliasPassword == null) {
            keyAliasPassword = keyStorePassword;
            if (isDebug()) {
                log("<keyAliasPassword> parameter not specified: will default to the <keyStorePassword> parameter.");
            }
        }
        
        String keyStoreType = getInitParameter("keyStoreType");
        if (keyStoreType == null) {
            if (isDebug()) {
                log("<type> parameter not specified: will default to \"" + KeyHolder.getDefaultType() + "\".");
            }
        }
        
        String keyAlias = getInitParameter("keyAlias");
        if (keyAlias == null) {
            if (isDebug()) {
                log("<keyAlias> parameter not specified: will look for the first one in the keystore.");
            }
        }
        
        if (isDebug()) {
            StringBuffer logBuffer =
            new StringBuffer(1024)
            .append("KeyStore related parameters:")
            .append("  keyStoreFileName=").append(keyStoreFileName)
            .append(", keyStoreType=").append(keyStoreType)
            .append(", keyAlias=").append(keyAlias)
            .append(" ");
            log(logBuffer.toString());
        }
            
        // Certificate preparation
        setKeyHolder(new KeyHolder(keyStoreFileName, keyStorePassword, keyAlias, keyAliasPassword, keyStoreType));
        
        if (isDebug()) {
            log("Subject Distinguished Name: " + getKeyHolder().getSignerDistinguishedName());
        }
        
        if (getKeyHolder().getSignerAddress() == null) {
            throw new MessagingException("Signer address missing in the certificate.");
        }
    
protected voidinitPostmasterSigns()
Initializer for property postmasterSigns.

        setPostmasterSigns((getInitParameter("postmasterSigns") == null) ? false : new Boolean(getInitParameter("postmasterSigns")).booleanValue());
    
protected voidinitRebuildFrom()
Initializer for property rebuildFrom.

        setRebuildFrom((getInitParameter("rebuildFrom") == null) ? false : new Boolean(getInitParameter("rebuildFrom")).booleanValue());
        if (isDebug()) {
            if (isRebuildFrom()) {
                log("Will modify the \"From:\" header.");
            } else {
                log("Will leave the \"From:\" header unchanged.");
            }
        }
    
protected voidinitSignerName()
Initializer for property signerName.

        setSignerName(getInitParameter("signerName"));
        if (getSignerName() == null) {
            if (getKeyHolder() == null) {
                throw new RuntimeException("initKeyHolder() must be invoked before initSignerName()");
            }
            setSignerName(getKeyHolder().getSignerCN());
            if (isDebug()) {
                log("<signerName> parameter not specified: will use the certificate signer \"CN=\" attribute.");
            }
        }
    
public booleanisDebug()
Getter for property debug.

return
Value of property debug.

        return this.debug;
    
protected booleanisOkToSign(org.apache.mailet.Mail mail)

Checks if the mail can be signed.

Rules:

  1. The reverse-path != null (it is not a bounce).
  2. The sender user must have been SMTP authenticated.
  3. Either:
    • The reverse-path is the postmaster address and {@link #isPostmasterSigns} returns true
    • or the reverse-path == the authenticated user and there is at least one "From:" address == reverse-path.
    • .
  4. The message has not already been signed (mimeType != multipart/signed and != application/pkcs7-mime).

param
mail The mail object to check.
return
True if can be signed.


        MailAddress reversePath = mail.getSender();
        
        // Is it a bounce?
        if (reversePath == null) {
            return false;
        }
        
        String authUser = (String) mail.getAttribute("org.apache.james.SMTPAuthUser");
        // was the sender user SMTP authorized?
        if (authUser == null) {
            return false;
        }
        
        // The sender is the postmaster?
        if (getMailetContext().getPostmaster().equals(reversePath)) {
            // should not sign postmaster sent messages?
            if (!isPostmasterSigns()) {
                return false;
            }
        } else {
            // is the reverse-path user different from the SMTP authorized user?
            if (!reversePath.getUser().equals(authUser)) {
                return false;
            }
            // is there no "From:" address same as the reverse-path?
            if (!fromAddressSameAsReverse(mail)) {
                return false;
            }
        }
        
        
        // if already signed return false
        MimeMessage mimeMessage = mail.getMessage();
        if (mimeMessage.isMimeType("multipart/signed")
            || mimeMessage.isMimeType("application/pkcs7-mime")) {
            return false;
        }
        
        return true;
    
public booleanisPostmasterSigns()
Getter for property postmasterSigns. If true will sign messages signed by the postmaster.

return
Value of property postmasterSigns.

        return this.postmasterSigns;
    
public booleanisRebuildFrom()
Getter for property rebuildFrom. If true will modify the "From:" header.

The modification is as follows: assuming that the signer mail address in the signer certificate is trusted-server@xxx.com> and that From: "John Smith" we will get From: "John Smith" " <trusted-server@xxx.com>.

If the "ReplyTo:" header is missing or empty it will be set to the original "From:" header.

Such modification is necessary to achieve a correct behaviour with some mail clients (e.g. Microsoft Outlook Express).

return
Value of property rebuildFrom.

        return this.rebuildFrom;
    
public voidservice(org.apache.mailet.Mail mail)
Service does the hard work, and signs

param
mail the mail to sign
throws
MessagingException if a problem arises signing the mail

        
        try {
            if (!isOkToSign(mail)) {
                return;
            }
            
            MimeBodyPart wrapperBodyPart = getWrapperBodyPart(mail);
            
            MimeMessage originalMessage = mail.getMessage();
            
            // do it
            MimeMultipart signedMimeMultipart;
            if (wrapperBodyPart != null) {
                signedMimeMultipart = getKeyHolder().generate(wrapperBodyPart);
            } else {
                signedMimeMultipart = getKeyHolder().generate(originalMessage);
            }
            
            MimeMessage newMessage = new MimeMessage(Session.getDefaultInstance(System.getProperties(),
            null));
            Enumeration headerEnum = originalMessage.getAllHeaderLines();
            while (headerEnum.hasMoreElements()) {
                newMessage.addHeaderLine((String) headerEnum.nextElement());
            }
            
            newMessage.setSender(new InternetAddress(getKeyHolder().getSignerAddress(), getSignerName()));
  
            if (isRebuildFrom()) {
                // builds a new "mixed" "From:" header
                InternetAddress modifiedFromIA = new InternetAddress(getKeyHolder().getSignerAddress(), mail.getSender().toString());
                newMessage.setFrom(modifiedFromIA);
                
                // if the original "ReplyTo:" header is missing sets it to the original "From:" header
                newMessage.setReplyTo(originalMessage.getReplyTo());
            }
            
            newMessage.setContent(signedMimeMultipart, signedMimeMultipart.getContentType());
            String messageId = originalMessage.getMessageID();
            newMessage.saveChanges();
            if (messageId != null) {
                newMessage.setHeader(RFC2822Headers.MESSAGE_ID, messageId);
            }
            
            mail.setMessage(newMessage);
            
            // marks this mail as server-signed
            mail.setAttribute(SMIMEAttributeNames.SMIME_SIGNING_MAILET, this.getClass().getName());
            // it is valid for us by definition (signed here by us)
            mail.setAttribute(SMIMEAttributeNames.SMIME_SIGNATURE_VALIDITY, "valid");
            
            // saves the trusted server signer address
            // warning: should be same as the mail address in the certificate, but it is not guaranteed
            mail.setAttribute(SMIMEAttributeNames.SMIME_SIGNER_ADDRESS, getKeyHolder().getSignerAddress());
            
            if (isDebug()) {
                log("Message signed, reverse-path: " + mail.getSender() + ", Id: " + messageId);
            }
            
        } catch (MessagingException me) {
            log("MessagingException found - could not sign!", me);
            throw me;
        } catch (Exception e) {
            log("Exception found", e);
            throw new MessagingException("Exception thrown - could not sign!", e);
        }
        
    
public voidsetDebug(boolean debug)
Setter for property debug.

param
debug New value of property debug.

        this.debug = debug;
    
public voidsetExplanationText(java.lang.String explanationText)
Setter for property explanationText.

param
explanationText New value of property explanationText.

        this.explanationText = explanationText;
    
protected voidsetKeyHolder(org.apache.james.security.KeyHolder keyHolder)
Setter for property keyHolder. It is protected instead of public for security reasons.

param
keyHolder New value of property keyHolder.

        this.keyHolder = keyHolder;
    
public voidsetPostmasterSigns(boolean postmasterSigns)
Setter for property postmasterSigns.

param
postmasterSigns New value of property postmasterSigns.

        this.postmasterSigns = postmasterSigns;
    
public voidsetRebuildFrom(boolean rebuildFrom)
Setter for property rebuildFrom.

param
rebuildFrom New value of property rebuildFrom.

        this.rebuildFrom = rebuildFrom;
    
public voidsetSignerName(java.lang.String signerName)
Setter for property signerName.

param
signerName New value of property signerName.

        this.signerName = signerName;