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  package org.apache.james.transport.matchers;
21  
22  import org.apache.mailet.GenericMatcher;
23  import org.apache.mailet.Mail;
24  
25  import javax.mail.MessagingException;
26  import javax.mail.Multipart;
27  import javax.mail.Part;
28  import javax.mail.internet.MimeMessage;
29  import java.io.IOException;
30  import java.util.ArrayList;
31  import java.util.Collection;
32  import java.util.Iterator;
33  import java.util.StringTokenizer;
34  import java.util.Locale;
35  import java.util.zip.ZipInputStream;
36  import java.util.zip.ZipEntry;
37  import java.io.InputStream;
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: 494012 $ $Date: 2007-01-08 10:23:58 +0000 (lun, 08 gen 2007) $
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      public void init() throws MessagingException {
89          /* sets up fileNameMasks variable by parsing the condition */
90          
91          StringTokenizer st = new StringTokenizer(getCondition(), ", ", false);
92          ArrayList theMasks = new ArrayList(20);
93          while (st.hasMoreTokens()) {
94              String fileName = st.nextToken();
95              
96              // check possible parameters at the beginning of the condition
97              if (theMasks.size() == 0 && fileName.equalsIgnoreCase(UNZIP_REQUEST_PARAMETER)) {
98                  unzipIsRequested = true;
99                  log("zip file analysis requested");
100                 continue;
101             }
102             if (theMasks.size() == 0 && fileName.equalsIgnoreCase(DEBUG_REQUEST_PARAMETER)) {
103                 isDebug = true;
104                 log("debug requested");
105                 continue;
106             }
107             Mask mask = new Mask(); 
108             if (fileName.startsWith("*")) {
109                 mask.suffixMatch = true;
110                 mask.matchString = fileName.substring(1);
111             } else {
112                 mask.suffixMatch = false;
113                 mask.matchString = fileName;
114             }
115             mask.matchString = cleanFileName(mask.matchString);
116             theMasks.add(mask);
117         }
118         masks = (Mask[])theMasks.toArray(new Mask[0]);
119     }
120 
121     /*** 
122      * Either every recipient is matching or neither of them.
123      * @throws MessagingException if no matching attachment is found and at least one exception was thrown
124      */
125     public Collection match(Mail mail) throws MessagingException {
126         
127         try {
128             MimeMessage message = mail.getMessage();
129             
130             if (matchFound(message)) {
131                 return mail.getRecipients(); // matching file found
132             } else {
133                 return null; // no matching attachment found
134             }
135             
136         } catch (Exception e) {
137             if (isDebug) {
138                 log("Malformed message", e);
139             }
140             throw new MessagingException("Malformed message", e);
141         }
142     }
143     
144     /***
145      * Checks if <I>part</I> matches with at least one of the <CODE>masks</CODE>.
146      */
147     protected boolean matchFound(Part part) throws Exception {
148         
149         /*
150          * if there is an attachment and no inline text,
151          * the content type can be anything
152          */
153         
154         if (part.getContentType() == null ||
155             part.getContentType().startsWith("multipart/alternative")) {
156             return false;
157         }
158         
159         Object content;
160         
161         try {
162             content = part.getContent();
163         } catch (UnsupportedEncodingException uee) {
164             // in this case it is not an attachment, so ignore it
165             return false;
166         }
167         
168         Exception anException = null;
169         
170         if (content instanceof Multipart) {
171             Multipart multipart = (Multipart) content;
172             for (int i = 0; i < multipart.getCount(); i++) {
173                 try {
174                     Part bodyPart = multipart.getBodyPart(i);
175                     if (matchFound(bodyPart)) {
176                         return true; // matching file found
177                     }
178                 } catch (MessagingException e) {
179                     anException = e;
180                 } // remember any messaging exception and process next bodypart
181             }
182         } else {
183             String fileName = part.getFileName();
184             if (fileName != null) {
185                 fileName = cleanFileName(fileName);
186                 // check the file name
187                 if (matchFound(fileName)) {
188                     if (isDebug) {
189                         log("matched " + fileName);
190                     }
191                     return true;
192                 }
193                 if (unzipIsRequested && fileName.endsWith(ZIP_SUFFIX) && matchFoundInZip(part)){
194                     return true;
195                 }
196             }
197         }
198         
199         // if no matching attachment was found and at least one exception was catched rethrow it up
200         if (anException != null) {
201             throw anException;
202         }
203         
204         return false;
205     }
206 
207     /***
208      * Checks if <I>fileName</I> matches with at least one of the <CODE>masks</CODE>.
209      */
210     protected boolean matchFound(String fileName) {
211         for (int j = 0; j < masks.length; j++) {
212             boolean fMatch;
213             Mask mask = masks[j];
214             
215             //XXX: file names in mail may contain directory - theoretically
216             if (mask.suffixMatch) {
217                 fMatch = fileName.endsWith(mask.matchString);
218             } else {
219                 fMatch = fileName.equals(mask.matchString);
220             }
221             if (fMatch) {
222                 return true; // matching file found
223             }
224         }
225         return false;
226     }
227     
228     /***
229      * Checks if <I>part</I> is a zip containing a file that matches with at least one of the <CODE>masks</CODE>.
230      */
231     protected boolean matchFoundInZip(Part part) throws MessagingException, IOException {
232         ZipInputStream zis = new ZipInputStream(part.getInputStream());
233         
234         try {
235             while (true) {
236                 ZipEntry zipEntry = zis.getNextEntry();
237                 if (zipEntry == null) {
238                     break;
239                 }
240                 String fileName = zipEntry.getName();
241                 if (matchFound(fileName)) {
242                     if (isDebug) {
243                         log("matched " + part.getFileName() + "(" + fileName + ")");
244                     }
245                     return true;
246                 }
247             }
248             return false;
249         } finally {
250             zis.close();
251         }
252     }
253 
254     /***
255      * Transforms <I>fileName<I> in a trimmed lowercase string usable for matching agains the masks.
256      */
257     protected String cleanFileName(String fileName) {
258         return fileName.toLowerCase(Locale.US).trim();
259     }
260 }
261