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
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
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();
130 } else {
131 return null;
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
149
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
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;
175 }
176 } catch (MessagingException e) {
177 anException = e;
178 }
179 }
180 } else {
181 String fileName = part.getFileName();
182 if (fileName != null) {
183 fileName = cleanFileName(fileName);
184
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
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
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;
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