View Javadoc

1   /************************************************************************
2    * Copyright (c) 2000-2006 The Apache Software Foundation.             *
3    * All rights reserved.                                                *
4    * ------------------------------------------------------------------- *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you *
6    * may not use this file except in compliance with the License. You    *
7    * may obtain a copy of the License at:                                *
8    *                                                                     *
9    *     http://www.apache.org/licenses/LICENSE-2.0                      *
10   *                                                                     *
11   * Unless required by applicable law or agreed to in writing, software *
12   * distributed under the License is distributed on an "AS IS" BASIS,   *
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or     *
14   * implied.  See the License for the specific language governing       *
15   * permissions and limitations under the License.                      *
16   ***********************************************************************/
17   
18  package org.apache.james.transport.matchers;
19  
20  import org.apache.mailet.GenericMatcher;
21  import org.apache.mailet.Mail;
22  
23  import javax.mail.MessagingException;
24  import javax.mail.Multipart;
25  import javax.mail.Part;
26  import javax.mail.internet.MimeMessage;
27  import java.io.IOException;
28  import java.util.ArrayList;
29  import java.util.Collection;
30  import java.util.Iterator;
31  import java.util.StringTokenizer;
32  import java.util.Locale;
33  import java.util.zip.ZipInputStream;
34  import java.util.zip.ZipEntry;
35  import java.io.InputStream;
36  import java.io.UnsupportedEncodingException;
37  
38  
39  /***
40   * <P>Checks if at least one attachment has a file name which matches any
41   * element of a comma-separated or space-separated list of file name masks.</P>
42   * <P>Syntax: <CODE>match="AttachmentFileNameIs=[-d] [-z] <I>masks</I>"</CODE></P>
43   * <P>The match is case insensitive.</P>
44   * <P>File name masks may start with a wildcard '*'.</P>
45   * <P>Multiple file name masks can be specified, e.g.: '*.scr,*.bat'.</P>
46   * <P>If '<CODE>-d</CODE>' is coded, some debug info will be logged.</P>
47   * <P>If '<CODE>-z</CODE>' is coded, the check will be non-recursively applied
48   * to the contents of any attached '*.zip' file.</P>
49   *
50   * @version CVS $Revision: 365582 $ $Date: 2006-01-03 08:51:21 +0000 (mar, 03 gen 2006) $
51   * @since 2.2.0
52   */
53  public class AttachmentFileNameIs extends GenericMatcher {
54      
55      /*** Unzip request parameter. */
56      protected static final String UNZIP_REQUEST_PARAMETER = "-z";
57      
58      /*** Debug request parameter. */
59      protected static final String DEBUG_REQUEST_PARAMETER = "-d";
60      
61      /*** Match string for zip files. */
62      protected static final String ZIP_SUFFIX = ".zip";
63      
64      /***
65       * represents a single parsed file name mask.
66       */
67      private static class Mask {
68          /*** true if the mask starts with a wildcard asterisk */
69          public boolean suffixMatch;
70          
71          /*** file name mask not including the wildcard asterisk */
72          public String matchString;
73      }
74      
75      /***
76       * Controls certain log messages.
77       */
78      protected boolean isDebug = false;
79  
80      /*** contains ParsedMask instances, setup by init */
81      private Mask[] masks = null;
82      
83      /*** True if unzip is requested. */
84      protected boolean unzipIsRequested;
85      
86      public void init() throws MessagingException {
87          /* sets up fileNameMasks variable by parsing the condition */
88          
89          StringTokenizer st = new StringTokenizer(getCondition(), ", ", false);
90          ArrayList theMasks = new ArrayList(20);
91          while (st.hasMoreTokens()) {
92              String fileName = st.nextToken();
93              
94              // check possible parameters at the beginning of the condition
95              if (theMasks.size() == 0 && fileName.equalsIgnoreCase(UNZIP_REQUEST_PARAMETER)) {
96                  unzipIsRequested = true;
97                  log("zip file analysis requested");
98                  continue;
99              }
100             if (theMasks.size() == 0 && fileName.equalsIgnoreCase(DEBUG_REQUEST_PARAMETER)) {
101                 isDebug = true;
102                 log("debug requested");
103                 continue;
104             }
105             Mask mask = new Mask(); 
106             if (fileName.startsWith("*")) {
107                 mask.suffixMatch = true;
108                 mask.matchString = fileName.substring(1);
109             } else {
110                 mask.suffixMatch = false;
111                 mask.matchString = fileName;
112             }
113             mask.matchString = cleanFileName(mask.matchString);
114             theMasks.add(mask);
115         }
116         masks = (Mask[])theMasks.toArray(new Mask[0]);
117     }
118 
119     /*** 
120      * Either every recipient is matching or neither of them.
121      * @throws MessagingException if no matching attachment is found and at least one exception was thrown
122      */
123     public Collection match(Mail mail) throws MessagingException {
124         
125         try {
126             MimeMessage message = mail.getMessage();
127             
128             if (matchFound(message)) {
129                 return mail.getRecipients(); // matching file found
130             } else {
131                 return null; // no matching attachment found
132             }
133             
134         } catch (Exception e) {
135             if (isDebug) {
136                 log("Malformed message", e);
137             }
138             throw new MessagingException("Malformed message", e);
139         }
140     }
141     
142     /***
143      * Checks if <I>part</I> matches with at least one of the <CODE>masks</CODE>.
144      */
145     protected boolean matchFound(Part part) throws Exception {
146         
147         /*
148          * if there is an attachment and no inline text,
149          * the content type can be anything
150          */
151         
152         if (part.getContentType() == null ||
153             part.getContentType().startsWith("multipart/alternative")) {
154             return false;
155         }
156         
157         Object content;
158         
159         try {
160             content = part.getContent();
161         } catch (UnsupportedEncodingException uee) {
162             // in this case it is not an attachment, so ignore it
163             return false;
164         }
165         
166         Exception anException = null;
167         
168         if (content instanceof Multipart) {
169             Multipart multipart = (Multipart) content;
170             for (int i = 0; i < multipart.getCount(); i++) {
171                 try {
172                     Part bodyPart = multipart.getBodyPart(i);
173                     if (matchFound(bodyPart)) {
174                         return true; // matching file found
175                     }
176                 } catch (MessagingException e) {
177                     anException = e;
178                 } // remember any messaging exception and process next bodypart
179             }
180         } else {
181             String fileName = part.getFileName();
182             if (fileName != null) {
183                 fileName = cleanFileName(fileName);
184                 // check the file name
185                 if (matchFound(fileName)) {
186                     if (isDebug) {
187                         log("matched " + fileName);
188                     }
189                     return true;
190                 }
191                 if (unzipIsRequested && fileName.endsWith(ZIP_SUFFIX) && matchFoundInZip(part)){
192                     return true;
193                 }
194             }
195         }
196         
197         // if no matching attachment was found and at least one exception was catched rethrow it up
198         if (anException != null) {
199             throw anException;
200         }
201         
202         return false;
203     }
204 
205     /***
206      * Checks if <I>fileName</I> matches with at least one of the <CODE>masks</CODE>.
207      */
208     protected boolean matchFound(String fileName) {
209         for (int j = 0; j < masks.length; j++) {
210             boolean fMatch;
211             Mask mask = masks[j];
212             
213             //XXX: file names in mail may contain directory - theoretically
214             if (mask.suffixMatch) {
215                 fMatch = fileName.endsWith(mask.matchString);
216             } else {
217                 fMatch = fileName.equals(mask.matchString);
218             }
219             if (fMatch) {
220                 return true; // matching file found
221             }
222         }
223         return false;
224     }
225     
226     /***
227      * Checks if <I>part</I> is a zip containing a file that matches with at least one of the <CODE>masks</CODE>.
228      */
229     protected boolean matchFoundInZip(Part part) throws MessagingException, IOException {
230         ZipInputStream zis = new ZipInputStream(part.getInputStream());
231         
232         try {
233             while (true) {
234                 ZipEntry zipEntry = zis.getNextEntry();
235                 if (zipEntry == null) {
236                     break;
237                 }
238                 String fileName = zipEntry.getName();
239                 if (matchFound(fileName)) {
240                     if (isDebug) {
241                         log("matched " + part.getFileName() + "(" + fileName + ")");
242                     }
243                     return true;
244                 }
245             }
246             return false;
247         } finally {
248             zis.close();
249         }
250     }
251 
252     /***
253      * Transforms <I>fileName<I> in a trimmed lowercase string usable for matching agains the masks.
254      */
255     protected String cleanFileName(String fileName) {
256         return fileName.toLowerCase(Locale.US).trim();
257     }
258 }
259