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 (Mon, 08 Jan 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
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
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();
132 } else {
133 return null;
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
151
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
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;
177 }
178 } catch (MessagingException e) {
179 anException = e;
180 }
181 }
182 } else {
183 String fileName = part.getFileName();
184 if (fileName != null) {
185 fileName = cleanFileName(fileName);
186
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
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
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;
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