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