001: /****************************************************************
002: * Licensed to the Apache Software Foundation (ASF) under one *
003: * or more contributor license agreements. See the NOTICE file *
004: * distributed with this work for additional information *
005: * regarding copyright ownership. The ASF licenses this file *
006: * to you under the Apache License, Version 2.0 (the *
007: * "License"); you may not use this file except in compliance *
008: * with the License. You may obtain a copy of the License at *
009: * *
010: * http://www.apache.org/licenses/LICENSE-2.0 *
011: * *
012: * Unless required by applicable law or agreed to in writing, *
013: * software distributed under the License is distributed on an *
014: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
015: * KIND, either express or implied. See the License for the *
016: * specific language governing permissions and limitations *
017: * under the License. *
018: ****************************************************************/package org.apache.james.transport.matchers;
019:
020: import org.apache.mailet.GenericMatcher;
021: import org.apache.mailet.Mail;
022:
023: import javax.mail.MessagingException;
024: import javax.mail.Multipart;
025: import javax.mail.Part;
026: import javax.mail.internet.MimeMessage;
027: import java.io.IOException;
028: import java.util.ArrayList;
029: import java.util.Collection;
030: import java.util.Iterator;
031: import java.util.StringTokenizer;
032: import java.util.Locale;
033: import java.util.zip.ZipInputStream;
034: import java.util.zip.ZipEntry;
035: import java.io.InputStream;
036: import java.io.UnsupportedEncodingException;
037:
038: /**
039: * <P>Checks if at least one attachment has a file name which matches any
040: * element of a comma-separated or space-separated list of file name masks.</P>
041: * <P>Syntax: <CODE>match="AttachmentFileNameIs=[-d] [-z] <I>masks</I>"</CODE></P>
042: * <P>The match is case insensitive.</P>
043: * <P>File name masks may start with a wildcard '*'.</P>
044: * <P>Multiple file name masks can be specified, e.g.: '*.scr,*.bat'.</P>
045: * <P>If '<CODE>-d</CODE>' is coded, some debug info will be logged.</P>
046: * <P>If '<CODE>-z</CODE>' is coded, the check will be non-recursively applied
047: * to the contents of any attached '*.zip' file.</P>
048: *
049: * @version CVS $Revision: 494012 $ $Date: 2007-01-08 11:23:58 +0100 (Mo, 08 Jan 2007) $
050: * @since 2.2.0
051: */
052: public class AttachmentFileNameIs extends GenericMatcher {
053:
054: /** Unzip request parameter. */
055: protected static final String UNZIP_REQUEST_PARAMETER = "-z";
056:
057: /** Debug request parameter. */
058: protected static final String DEBUG_REQUEST_PARAMETER = "-d";
059:
060: /** Match string for zip files. */
061: protected static final String ZIP_SUFFIX = ".zip";
062:
063: /**
064: * represents a single parsed file name mask.
065: */
066: private static class Mask {
067: /** true if the mask starts with a wildcard asterisk */
068: public boolean suffixMatch;
069:
070: /** file name mask not including the wildcard asterisk */
071: public String matchString;
072: }
073:
074: /**
075: * Controls certain log messages.
076: */
077: protected boolean isDebug = false;
078:
079: /** contains ParsedMask instances, setup by init */
080: private Mask[] masks = null;
081:
082: /** True if unzip is requested. */
083: protected boolean unzipIsRequested;
084:
085: public void init() throws MessagingException {
086: /* sets up fileNameMasks variable by parsing the condition */
087:
088: StringTokenizer st = new StringTokenizer(getCondition(), ", ",
089: false);
090: ArrayList theMasks = new ArrayList(20);
091: while (st.hasMoreTokens()) {
092: String fileName = st.nextToken();
093:
094: // check possible parameters at the beginning of the condition
095: if (theMasks.size() == 0
096: && fileName
097: .equalsIgnoreCase(UNZIP_REQUEST_PARAMETER)) {
098: unzipIsRequested = true;
099: log("zip file analysis requested");
100: continue;
101: }
102: if (theMasks.size() == 0
103: && fileName
104: .equalsIgnoreCase(DEBUG_REQUEST_PARAMETER)) {
105: isDebug = true;
106: log("debug requested");
107: continue;
108: }
109: Mask mask = new Mask();
110: if (fileName.startsWith("*")) {
111: mask.suffixMatch = true;
112: mask.matchString = fileName.substring(1);
113: } else {
114: mask.suffixMatch = false;
115: mask.matchString = fileName;
116: }
117: mask.matchString = cleanFileName(mask.matchString);
118: theMasks.add(mask);
119: }
120: masks = (Mask[]) theMasks.toArray(new Mask[0]);
121: }
122:
123: /**
124: * Either every recipient is matching or neither of them.
125: * @throws MessagingException if no matching attachment is found and at least one exception was thrown
126: */
127: public Collection match(Mail mail) throws MessagingException {
128:
129: try {
130: MimeMessage message = mail.getMessage();
131:
132: if (matchFound(message)) {
133: return mail.getRecipients(); // matching file found
134: } else {
135: return null; // no matching attachment found
136: }
137:
138: } catch (Exception e) {
139: if (isDebug) {
140: log("Malformed message", e);
141: }
142: throw new MessagingException("Malformed message", e);
143: }
144: }
145:
146: /**
147: * Checks if <I>part</I> matches with at least one of the <CODE>masks</CODE>.
148: */
149: protected boolean matchFound(Part part) throws Exception {
150:
151: /*
152: * if there is an attachment and no inline text,
153: * the content type can be anything
154: */
155:
156: if (part.getContentType() == null
157: || part.getContentType().startsWith(
158: "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)
197: && matchFoundInZip(part)) {
198: return true;
199: }
200: }
201: }
202:
203: // if no matching attachment was found and at least one exception was catched rethrow it up
204: if (anException != null) {
205: throw anException;
206: }
207:
208: return false;
209: }
210:
211: /**
212: * Checks if <I>fileName</I> matches with at least one of the <CODE>masks</CODE>.
213: */
214: protected boolean matchFound(String fileName) {
215: for (int j = 0; j < masks.length; j++) {
216: boolean fMatch;
217: Mask mask = masks[j];
218:
219: //XXX: file names in mail may contain directory - theoretically
220: if (mask.suffixMatch) {
221: fMatch = fileName.endsWith(mask.matchString);
222: } else {
223: fMatch = fileName.equals(mask.matchString);
224: }
225: if (fMatch) {
226: return true; // matching file found
227: }
228: }
229: return false;
230: }
231:
232: /**
233: * Checks if <I>part</I> is a zip containing a file that matches with at least one of the <CODE>masks</CODE>.
234: */
235: protected boolean matchFoundInZip(Part part)
236: throws MessagingException, IOException {
237: ZipInputStream zis = new ZipInputStream(part.getInputStream());
238:
239: try {
240: while (true) {
241: ZipEntry zipEntry = zis.getNextEntry();
242: if (zipEntry == null) {
243: break;
244: }
245: String fileName = zipEntry.getName();
246: if (matchFound(fileName)) {
247: if (isDebug) {
248: log("matched " + part.getFileName() + "("
249: + fileName + ")");
250: }
251: return true;
252: }
253: }
254: return false;
255: } finally {
256: zis.close();
257: }
258: }
259:
260: /**
261: * Transforms <I>fileName<I> in a trimmed lowercase string usable for matching agains the masks.
262: */
263: protected String cleanFileName(String fileName) {
264: return fileName.toLowerCase(Locale.US).trim();
265: }
266: }
|