View Javadoc

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