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