001    /**
002     *
003     * Copyright 2003-2004 The Apache Software Foundation
004     *
005     *  Licensed under the Apache License, Version 2.0 (the "License");
006     *  you may not use this file except in compliance with the License.
007     *  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     *  Unless required by applicable law or agreed to in writing, software
012     *  distributed under the License is distributed on an "AS IS" BASIS,
013     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     *  See the License for the specific language governing permissions and
015     *  limitations under the License.
016     */
017    
018    package javax.mail.internet;
019    
020    import java.io.IOException;
021    import java.io.InputStream;
022    import java.io.OutputStream;
023    import java.util.ArrayList;
024    import java.util.Arrays;
025    import java.util.Collections;
026    import java.util.Enumeration;
027    import java.util.HashSet;
028    import java.util.Iterator;
029    import java.util.LinkedHashMap;
030    import java.util.List;
031    import java.util.Map;
032    import java.util.Set;
033    
034    import javax.mail.Address;
035    import javax.mail.Header;
036    import javax.mail.MessagingException;
037    
038    /**
039     * Class that represents the RFC822 headers associated with a message.
040     *
041     * @version $Rev: 398618 $ $Date: 2006-05-01 08:18:57 -0700 (Mon, 01 May 2006) $
042     */
043    public class InternetHeaders {
044        // RFC822 imposes an ordering on its headers so we use a LinkedHashedMap
045        private final LinkedHashMap headers = new LinkedHashMap();
046    
047        private transient String lastHeaderName;
048    
049        /**
050         * Create an empty InternetHeaders
051         */
052        public InternetHeaders() {
053            // we need to initialize the headers in the correct order
054            // fields: dates source destination optional-field
055            // dates:
056            setHeaderList("Date", null);
057            setHeaderList("Resent-Date", null);
058            // source: trace originator resent
059            // trace: return received
060            setHeaderList("Return-path", null);
061            setHeaderList("Received", null);
062            // originator: authentic reply-to
063            setHeaderList("Sender", null);
064            setHeaderList("From", null);
065            setHeaderList("Reply-To", null);
066            // resent: resent-authentic resent-reply-to
067            setHeaderList("Resent-Sender", null);
068            setHeaderList("Resent-From", null);
069            setHeaderList("Resent-Reply-To", null);
070            // destination:
071            setHeaderList("To", null);
072            setHeaderList("Resent-To", null);
073            setHeaderList("cc", null);
074            setHeaderList("Resent-cc", null);
075            setHeaderList("bcc", null);
076            setHeaderList("Resent-bcc", null);
077            // optional-field:
078            setHeaderList("Message-ID", null);
079            setHeaderList("Resent-Message-ID", null);
080            setHeaderList("In-Reply-To", null);
081            setHeaderList("References", null);
082            setHeaderList("Keywords", null);
083            setHeaderList("Subject", null);
084            setHeaderList("Comments", null);
085            setHeaderList("Encrypted", null);
086        }
087    
088        /**
089         * Create a new InternetHeaders initialized by reading headers from the
090         * stream.
091         *
092         * @param in
093         *            the RFC822 input stream to load from
094         * @throws MessagingException
095         *             if there is a problem pasring the stream
096         */
097        public InternetHeaders(InputStream in) throws MessagingException {
098            load(in);
099        }
100    
101        /**
102         * Read and parse the supplied stream and add all headers to the current
103         * set.
104         *
105         * @param in
106         *            the RFC822 input stream to load from
107         * @throws MessagingException
108         *             if there is a problem pasring the stream
109         */
110        public void load(InputStream in) throws MessagingException {
111            try {
112                StringBuffer name = new StringBuffer(32);
113                StringBuffer value = new StringBuffer(128);
114                done: while (true) {
115                    int c = in.read();
116                    char ch = (char) c;
117                    if (c == -1) {
118                        break;
119                    } else if (c == 13) {
120                        // empty line terminates header
121                        in.read(); // skip LF
122                        break;
123                    } else if (Character.isWhitespace(ch)) {
124                        // handle continuation
125                        do {
126                            c = in.read();
127                            if (c == -1) {
128                                break done;
129                            }
130                            ch = (char) c;
131                        } while (Character.isWhitespace(ch));
132                    } else {
133                        // new header
134                        if (name.length() > 0) {
135                            addHeader(name.toString().trim(), value.toString().trim());
136                        }
137                        name.setLength(0);
138                        value.setLength(0);
139                        while (true) {
140                            name.append((char) c);
141                            c = in.read();
142                            if (c == -1) {
143                                break done;
144                            } else if (c == ':') {
145                                break;
146                            }
147                        }
148                        c = in.read();
149                        if (c == -1) {
150                            break done;
151                        }
152                    }
153    
154                    while (c != 13) {
155                        ch = (char) c;
156                        value.append(ch);
157                        c = in.read();
158                        if (c == -1) {
159                            break done;
160                        }
161                    }
162                    // skip LF
163                    c = in.read();
164                    if (c == -1) {
165                        break;
166                    }
167                }
168                if (name.length() > 0) {
169                    addHeader(name.toString().trim(), value.toString().trim());
170                }
171            } catch (IOException e) {
172                throw new MessagingException("Error loading headers", e);
173            }
174        }
175    
176        /**
177         * Return all the values for the specified header.
178         *
179         * @param name
180         *            the header to return
181         * @return the values for that header, or null if the header is not present
182         */
183        public String[] getHeader(String name) {
184            List headers = getHeaderList(name);
185            if (headers == null) {
186                return null;
187            } else {
188                String[] result = new String[headers.size()];
189                for (int i = 0; i < headers.size(); i++) {
190                    InternetHeader header = (InternetHeader) headers.get(i);
191                    result[i] = header.getValue();
192                }
193                return result;
194            }
195        }
196    
197        /**
198         * Return the values for the specified header as a single String. If the
199         * header has more than one value then all values are concatenated together
200         * separated by the supplied delimiter.
201         *
202         * @param name
203         *            the header to return
204         * @param delimiter
205         *            the delimiter used in concatenation
206         * @return the header as a single String
207         */
208        public String getHeader(String name, String delimiter) {
209            List list = getHeaderList(name);
210            if (list == null) {
211                return null;
212            } else if (list.isEmpty()) {
213                return "";
214            } else if (list.size() == 1 || delimiter == null) {
215                return ((InternetHeader) list.get(0)).getValue();
216            } else {
217                StringBuffer buf = new StringBuffer(20 * list.size());
218                buf.append(((InternetHeader) list.get(0)).getValue());
219                for (int i = 1; i < list.size(); i++) {
220                    buf.append(delimiter);
221                    buf.append(((InternetHeader) list.get(i)).getValue());
222                }
223                return buf.toString();
224            }
225        }
226    
227        /**
228         * Set the value of the header to the supplied value; any existing headers
229         * are removed.
230         *
231         * @param name
232         *            the name of the header
233         * @param value
234         *            the new value
235         */
236        public void setHeader(String name, String value) {
237            List list = new ArrayList();
238            list.add(new InternetHeader(name, value));
239            setHeaderList(name, list);
240        }
241    
242        /**
243         * Add a new value to the header with the supplied name.
244         *
245         * @param name
246         *            the name of the header to add a new value for
247         * @param value
248         *            another value
249         */
250        public void addHeader(String name, String value) {
251            List list = getHeaderList(name);
252            if (list == null) {
253                list = new ArrayList();
254                headers.put(name.toLowerCase(), list);
255            }
256            list.add(new InternetHeader(name, value));
257        }
258    
259        /**
260         * Remove all header entries with the supplied name
261         *
262         * @param name
263         *            the header to remove
264         */
265        public void removeHeader(String name) {
266            List list = getHeaderList(name);
267            // it's possible we might not have the named header.  This is a nop if the header doesn't exist.
268            if (list != null) {
269                list.clear();
270            }
271        }
272    
273        /**
274         * Return all headers.
275         *
276         * @return an Enumeration<Header> containing all headers
277         */
278        public Enumeration getAllHeaders() {
279            List result = new ArrayList(headers.size() * 2);
280            Iterator it = headers.values().iterator();
281            while (it.hasNext()) {
282                List list = (List) it.next();
283                if (list != null) {
284                    result.addAll(list);
285                }
286            }
287            return Collections.enumeration(result);
288        }
289    
290        /**
291         * Return all matching Header objects.
292         */
293        public Enumeration getMatchingHeaders(String[] names) {
294            Set include = new HashSet(names.length);
295            for (int i = 0; i < names.length; i++) {
296                String name = names[i];
297                include.add(name.toLowerCase());
298            }
299            List result = new ArrayList(headers.size());
300            for (Iterator i = headers.entrySet().iterator(); i.hasNext();) {
301                Map.Entry entry = (Map.Entry) i.next();
302                if (entry.getValue() != null && include.contains(((String) entry.getKey()).toLowerCase())) {
303                    result.addAll((List) entry.getValue());
304                }
305            }
306            return Collections.enumeration(result);
307        }
308    
309        /**
310         * Return all non matching Header objects.
311         */
312        public Enumeration getNonMatchingHeaders(String[] names) {
313            Set exclude = new HashSet(names.length);
314            for (int i = 0; i < names.length; i++) {
315                String name = names[i];
316                exclude.add(name.toLowerCase());
317            }
318            List result = new ArrayList(headers.size());
319            for (Iterator i = headers.entrySet().iterator(); i.hasNext();) {
320                Map.Entry entry = (Map.Entry) i.next();
321                if (entry.getValue() != null && !exclude.contains(((String) entry.getKey()).toLowerCase())) {
322                    result.addAll((List) entry.getValue());
323                }
324            }
325            return Collections.enumeration(result);
326        }
327    
328        /**
329         * Add an RFC822 header line to the header store. If the line starts with a
330         * space or tab (a continuation line), add it to the last header line in the
331         * list. Otherwise, append the new header line to the list.
332         *
333         * Note that RFC822 headers can only contain US-ASCII characters
334         *
335         * @param line
336         *            raw RFC822 header line
337         */
338        public void addHeaderLine(String line) {
339            StringBuffer name = new StringBuffer(32);
340            StringBuffer value = new StringBuffer(128);
341            boolean inName = true;
342            boolean continuation = false;
343            if (Character.isWhitespace(line.charAt(0))) {
344                continuation = true;
345                inName = false;
346            }
347            for (int i = 0; i < line.length(); i++) {
348                char c = line.charAt(i);
349                if (inName && c == ':') {
350                    inName = false;
351                } else if (inName) {
352                    name.append(c);
353                } else {
354                    value.append(c);
355                }
356            }
357            if (continuation) {
358                List list = getHeaderList(lastHeaderName);
359                Header h = (Header) list.remove(list.size() - 1);
360                list.add(new InternetHeader(lastHeaderName, (h.getValue() + value.toString()).trim()));
361            } else {
362                lastHeaderName = name.toString().trim();
363                addHeader(lastHeaderName, value.toString().trim());
364            }
365        }
366    
367        /**
368         * Return all the header lines as an Enumeration of Strings.
369         */
370        public Enumeration getAllHeaderLines() {
371            return new HeaderLineEnumeration(getAllHeaders());
372        }
373    
374        /**
375         * Return all matching header lines as an Enumeration of Strings.
376         */
377        public Enumeration getMatchingHeaderLines(String[] names) {
378            return new HeaderLineEnumeration(getMatchingHeaders(names));
379        }
380    
381        /**
382         * Return all non-matching header lines.
383         */
384        public Enumeration getNonMatchingHeaderLines(String[] names) {
385            return new HeaderLineEnumeration(getNonMatchingHeaders(names));
386        }
387    
388        void setHeader(String name, Address[] addresses) {
389            List list = new ArrayList(addresses.length);
390            for (int i = 0; i < addresses.length; i++) {
391                Address address = addresses[i];
392                list.add(new InternetHeader(name, address.toString()));
393            }
394            headers.put(name.toLowerCase(), list);
395        }
396    
397        private List getHeaderList(String name) {
398            return (List) headers.get(name.toLowerCase());
399        }
400    
401        private void setHeaderList(String name, List list) {
402            headers.put(name.toLowerCase(), list);
403        }
404    
405        void writeTo(OutputStream out, String[] ignore) throws IOException {
406            Map map = new LinkedHashMap(headers);
407            if (ignore != null) {
408                // remove each of these from the header list (note, they are stored as lower case keys).
409                for (int i = 0; i < ignore.length; i++) {
410                    String key = ignore[i].toLowerCase();
411                    map.remove(key);
412                }
413            }
414            for (Iterator i = map.entrySet().iterator(); i.hasNext();) {
415                Map.Entry entry = (Map.Entry) i.next();
416                String name = (String) entry.getKey();
417                List headers = (List) entry.getValue();
418                if (headers != null) {
419                    for (int j = 0; j < headers.size(); j++) {
420                        InternetHeader header = (InternetHeader) headers.get(j);
421                        out.write(header.getName().getBytes());
422                        out.write(':');
423                        out.write(' ');
424                        out.write(header.getValue().getBytes());
425                        out.write(13);
426                        out.write(10);
427                    }
428                }
429            }
430        }
431    
432        private static class InternetHeader extends Header {
433            public InternetHeader(String name, String value) {
434                super(name, value);
435            }
436    
437            public boolean equals(Object obj) {
438                if (this == obj)
439                    return true;
440                if (obj instanceof InternetHeader == false)
441                    return false;
442                final InternetHeader other = (InternetHeader) obj;
443                return getName().equalsIgnoreCase(other.getName());
444            }
445    
446            public int hashCode() {
447                return getName().toLowerCase().hashCode();
448            }
449        }
450    
451        private static class HeaderLineEnumeration implements Enumeration {
452            private Enumeration headers;
453    
454            public HeaderLineEnumeration(Enumeration headers) {
455                this.headers = headers;
456            }
457    
458            public boolean hasMoreElements() {
459                return headers.hasMoreElements();
460            }
461    
462            public Object nextElement() {
463                Header h = (Header) headers.nextElement();
464                return h.getName() + ": " + h.getValue();
465            }
466        }
467    }