001    /**
002     *
003     * Copyright 2003-2006 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: 421852 $ $Date: 2006-07-14 03:02:19 -0700 (Fri, 14 Jul 2006) $
042     */
043    public class InternetHeaders {
044        // the list of headers (to preserve order);
045        protected List headers = new ArrayList();
046    
047        private transient String lastHeaderName;
048    
049        /**
050         * Create an empty InternetHeaders
051         */
052        public InternetHeaders() {
053            // these are created in the preferred order of the headers.
054            addHeader("Return-path", null);
055            addHeader("Received", null);
056            addHeader("Message-ID", null);
057            addHeader("Resent-Date", null);
058            addHeader("Date", null);
059            addHeader("Resent-From", null);
060            addHeader("From", null);
061            addHeader("Reply-To", null);
062            addHeader("Sender", null);
063            addHeader("To", null);
064            addHeader("Subject", null);
065            addHeader("Cc", null);
066            addHeader("In-Reply-To", null);
067            addHeader("Resent-Message-Id", null);
068            addHeader("Errors-To", null);
069            addHeader("MIME-Version", null);
070            addHeader("Content-Type", null);
071            addHeader("Content-Transfer-Encoding", null);
072            addHeader("Content-MD5", null);
073            // the following is a special marker used to identify new header insertion points.
074            addHeader(":", null);
075            addHeader("Content-Length", null);
076            addHeader("Status", null);
077        }
078    
079        /**
080         * Create a new InternetHeaders initialized by reading headers from the
081         * stream.
082         *
083         * @param in
084         *            the RFC822 input stream to load from
085         * @throws MessagingException
086         *             if there is a problem pasring the stream
087         */
088        public InternetHeaders(InputStream in) throws MessagingException {
089            load(in);
090        }
091    
092        /**
093         * Read and parse the supplied stream and add all headers to the current
094         * set.
095         *
096         * @param in
097         *            the RFC822 input stream to load from
098         * @throws MessagingException
099         *             if there is a problem pasring the stream
100         */
101        public void load(InputStream in) throws MessagingException {
102            try {
103                StringBuffer name = new StringBuffer(32);
104                StringBuffer value = new StringBuffer(128);
105                done: while (true) {
106                    int c = in.read();
107                    char ch = (char) c;
108                    if (c == -1) {
109                        break;
110                    } else if (c == 13) {
111                        // empty line terminates header
112                        in.read(); // skip LF
113                        break;
114                    } else if (Character.isWhitespace(ch)) {
115                        // handle continuation
116                        do {
117                            c = in.read();
118                            if (c == -1) {
119                                break done;
120                            }
121                            ch = (char) c;
122                        } while (Character.isWhitespace(ch));
123                    } else {
124                        // new header
125                        if (name.length() > 0) {
126                            addHeader(name.toString().trim(), value.toString().trim());
127                        }
128                        name.setLength(0);
129                        value.setLength(0);
130                        while (true) {
131                            name.append((char) c);
132                            c = in.read();
133                            if (c == -1) {
134                                break done;
135                            } else if (c == ':') {
136                                break;
137                            }
138                        }
139                        c = in.read();
140                        if (c == -1) {
141                            break done;
142                        }
143                    }
144    
145                    while (c != 13) {
146                        ch = (char) c;
147                        value.append(ch);
148                        c = in.read();
149                        if (c == -1) {
150                            break done;
151                        }
152                    }
153                    // skip LF
154                    c = in.read();
155                    if (c == -1) {
156                        break;
157                    }
158                }
159                if (name.length() > 0) {
160                    addHeader(name.toString().trim(), value.toString().trim());
161                }
162            } catch (IOException e) {
163                throw new MessagingException("Error loading headers", e);
164            }
165        }
166    
167    
168        /**
169         * Return all the values for the specified header.
170         *
171         * @param name
172         *            the header to return
173         * @return the values for that header, or null if the header is not present
174         */
175        public String[] getHeader(String name) {
176            List accumulator = new ArrayList();
177    
178            for (int i = 0; i < headers.size(); i++) {
179                InternetHeader header = (InternetHeader)headers.get(i);
180                if (header.getName().equalsIgnoreCase(name) && header.getValue() != null) {
181                    accumulator.add(header.getValue());
182                }
183            }
184    
185            // this is defined as returning null of nothing is found.
186            if (accumulator.isEmpty()) {
187                return null;
188            }
189    
190            // convert this to an array.
191            return (String[])accumulator.toArray(new String[accumulator.size()]);
192        }
193    
194        /**
195         * Return the values for the specified header as a single String. If the
196         * header has more than one value then all values are concatenated together
197         * separated by the supplied delimiter.
198         *
199         * @param name
200         *            the header to return
201         * @param delimiter
202         *            the delimiter used in concatenation
203         * @return the header as a single String
204         */
205        public String getHeader(String name, String delimiter) {
206            // get all of the headers with this name
207            String[] matches = getHeader(name);
208    
209            // no match?  return a null.
210            if (matches == null) {
211                return null;
212            }
213    
214            // a null delimiter means just return the first one.  If there's only one item, this is easy too.
215            if (matches.length == 1 || delimiter == null) {
216                return matches[0];
217            }
218    
219            // perform the concatenation
220            StringBuffer result = new StringBuffer(matches[0]);
221    
222            for (int i = 1; i < matches.length; i++) {
223                result.append(delimiter);
224                result.append(matches[i]);
225            }
226    
227            return result.toString();
228        }
229    
230    
231        /**
232         * Set the value of the header to the supplied value; any existing headers
233         * are removed.
234         *
235         * @param name
236         *            the name of the header
237         * @param value
238         *            the new value
239         */
240        public void setHeader(String name, String value) {
241            // look for a header match
242            for (int i = 0; i < headers.size(); i++) {
243                InternetHeader header = (InternetHeader)headers.get(i);
244                // found a matching header
245                if (name.equalsIgnoreCase(header.getName())) {
246                    // just update the header value
247                    header.setValue(value);
248                    // remove all of the headers from this point
249                    removeHeaders(name, i + 1);
250                    return;
251                }
252            }
253    
254            // doesn't exist, so process as an add.
255            addHeader(name, value);
256        }
257    
258    
259        /**
260         * Remove all headers with the given name, starting with the
261         * specified start position.
262         *
263         * @param name   The target header name.
264         * @param pos    The position of the first header to examine.
265         */
266        private void removeHeaders(String name, int pos) {
267            // now go remove all other instances of this header
268            for (int i = pos; i < headers.size(); i++) {
269                InternetHeader header = (InternetHeader)headers.get(i);
270                // found a matching header
271                if (name.equalsIgnoreCase(header.getName())) {
272                    // remove this item, and back up
273                    headers.remove(i);
274                    i--;
275                }
276            }
277        }
278    
279    
280        /**
281         * Find a header in the current list by name, returning the index.
282         *
283         * @param name   The target name.
284         *
285         * @return The index of the header in the list.  Returns -1 for a not found
286         *         condition.
287         */
288        private int findHeader(String name) {
289            return findHeader(name, 0);
290        }
291    
292    
293        /**
294         * Find a header in the current list, beginning with the specified
295         * start index.
296         *
297         * @param name   The target header name.
298         * @param start  The search start index.
299         *
300         * @return The index of the first matching header.  Returns -1 if the
301         *         header is not located.
302         */
303        private int findHeader(String name, int start) {
304            for (int i = start; i < headers.size(); i++) {
305                InternetHeader header = (InternetHeader)headers.get(i);
306                // found a matching header
307                if (name.equalsIgnoreCase(header.getName())) {
308                    return i;
309                }
310            }
311            return -1;
312        }
313    
314        /**
315         * Add a new value to the header with the supplied name.
316         *
317         * @param name
318         *            the name of the header to add a new value for
319         * @param value
320         *            another value
321         */
322        public void addHeader(String name, String value) {
323            InternetHeader newHeader = new InternetHeader(name, value);
324    
325            // The javamail spec states that "Recieved" headers need to be added in reverse order.
326            // Return-Path is permitted before Received, so handle it the same way.
327            if (name.equalsIgnoreCase("Received") || name.equalsIgnoreCase("Return-Path")) {
328                // see if we have one of these already
329                int pos = findHeader(name);
330    
331                // either insert before an existing header, or insert at the very beginning
332                if (pos != -1) {
333                    // this could be a placeholder header with a null value.  If it is, just update
334                    // the value.  Otherwise, insert in front of the existing header.
335                    InternetHeader oldHeader = (InternetHeader)headers.get(pos);
336                    if (oldHeader.getValue() == null) {
337                        oldHeader.setValue(value);
338                    }
339                    else {
340                        headers.add(pos, newHeader);
341                    }
342                }
343                else {
344                    // doesn't exist, so insert at the beginning
345                    headers.add(0, newHeader);
346                }
347            }
348            // normal insertion
349            else {
350                // see if we have one of these already
351                int pos = findHeader(name);
352    
353                // either insert before an existing header, or insert at the very beginning
354                if (pos != -1) {
355                    InternetHeader oldHeader = (InternetHeader)headers.get(pos);
356                    // if the existing header is a place holder, we can just update the value
357                    if (oldHeader.getValue() == null) {
358                        oldHeader.setValue(value);
359                    }
360                    else {
361                        // we have at least one existing header with this name.  We need to find the last occurrance,
362                        // and insert after that spot.
363    
364                        int lastPos = findHeader(name, pos + 1);
365    
366                        while (lastPos != -1) {
367                            pos = lastPos;
368                            lastPos = findHeader(name, pos + 1);
369                        }
370    
371                        // ok, we have the insertion position
372                        headers.add(pos + 1, newHeader);
373                    }
374                }
375                else {
376                    // find the insertion marker.  If that is missing somehow, insert at the end.
377                    pos = findHeader(":");
378                    if (pos == -1) {
379                        pos = headers.size();
380                    }
381                    headers.add(pos, newHeader);
382                }
383            }
384        }
385    
386    
387        /**
388         * Remove all header entries with the supplied name
389         *
390         * @param name
391         *            the header to remove
392         */
393        public void removeHeader(String name) {
394            // the first occurrance of a header is just zeroed out.
395            int pos = findHeader(name);
396    
397            if (pos != -1) {
398                InternetHeader oldHeader = (InternetHeader)headers.get(pos);
399                // keep the header in the list, but with a null value
400                oldHeader.setValue(null);
401                // now remove all other headers with this name
402                removeHeaders(name, pos + 1);
403            }
404        }
405    
406    
407        /**
408         * Return all headers.
409         *
410         * @return an Enumeration<Header> containing all headers
411         */
412        public Enumeration getAllHeaders() {
413            List result = new ArrayList();
414    
415            for (int i = 0; i < headers.size(); i++) {
416                InternetHeader header = (InternetHeader)headers.get(i);
417                // we only include headers with real values, no placeholders
418                if (header.getValue() != null) {
419                    result.add(header);
420                }
421            }
422            // just return a list enumerator for the header list.
423            return Collections.enumeration(result);
424        }
425    
426    
427        /**
428         * Test if a given header name is a match for any header in the
429         * given list.
430         *
431         * @param name   The name of the current tested header.
432         * @param names  The list of names to match against.
433         *
434         * @return True if this is a match for any name in the list, false
435         *         for a complete mismatch.
436         */
437        private boolean matchHeader(String name, String[] names) {
438            for (int i = 0; i < names.length; i++) {
439                if (name.equalsIgnoreCase(names[i])) {
440                    return true;
441                }
442            }
443            return false;
444        }
445    
446    
447        /**
448         * Return all matching Header objects.
449         */
450        public Enumeration getMatchingHeaders(String[] names) {
451            List result = new ArrayList();
452    
453            for (int i = 0; i < headers.size(); i++) {
454                InternetHeader header = (InternetHeader)headers.get(i);
455                // we only include headers with real values, no placeholders
456                if (header.getValue() != null) {
457                    // only add the matching ones
458                    if (matchHeader(header.getName(), names)) {
459                        result.add(header);
460                    }
461                }
462            }
463            return Collections.enumeration(result);
464        }
465    
466    
467        /**
468         * Return all non matching Header objects.
469         */
470        public Enumeration getNonMatchingHeaders(String[] names) {
471            List result = new ArrayList();
472    
473            for (int i = 0; i < headers.size(); i++) {
474                InternetHeader header = (InternetHeader)headers.get(i);
475                // we only include headers with real values, no placeholders
476                if (header.getValue() != null) {
477                    // only add the non-matching ones
478                    if (!matchHeader(header.getName(), names)) {
479                        result.add(header);
480                    }
481                }
482            }
483            return Collections.enumeration(result);
484        }
485    
486    
487        /**
488         * Add an RFC822 header line to the header store. If the line starts with a
489         * space or tab (a continuation line), add it to the last header line in the
490         * list. Otherwise, append the new header line to the list.
491         *
492         * Note that RFC822 headers can only contain US-ASCII characters
493         *
494         * @param line
495         *            raw RFC822 header line
496         */
497        public void addHeaderLine(String line) {
498            // null lines are a nop
499            if (line.length() == 0) {
500                return;
501            }
502    
503            // we need to test the first character to see if this is a continuation whitespace
504            char ch = line.charAt(0);
505    
506            // tabs and spaces are special.  This is a continuation of the last header in the list.
507            if (ch == ' ' || ch == '\t') {
508                InternetHeader header = (InternetHeader)headers.get(headers.size() - 1);
509                header.appendValue(line);
510            }
511            else {
512                // this just gets appended to the end, preserving the addition order.
513                headers.add(new InternetHeader(line));
514            }
515        }
516    
517    
518        /**
519         * Return all the header lines as an Enumeration of Strings.
520         */
521        public Enumeration getAllHeaderLines() {
522            return new HeaderLineEnumeration(getAllHeaders());
523        }
524    
525        /**
526         * Return all matching header lines as an Enumeration of Strings.
527         */
528        public Enumeration getMatchingHeaderLines(String[] names) {
529            return new HeaderLineEnumeration(getMatchingHeaders(names));
530        }
531    
532        /**
533         * Return all non-matching header lines.
534         */
535        public Enumeration getNonMatchingHeaderLines(String[] names) {
536            return new HeaderLineEnumeration(getNonMatchingHeaders(names));
537        }
538    
539    
540        /**
541         * Set an internet header from a list of addresses.  The
542         * first address item is set, followed by a series of addHeaders().
543         *
544         * @param name      The name to set.
545         * @param addresses The list of addresses to set.
546         */
547        void setHeader(String name, Address[] addresses) {
548            // if this is empty, then ew need to replace this
549            if (addresses.length == 0) {
550                removeHeader(name);
551            }
552    
553            // replace the first header
554            setHeader(name, addresses[0].toString());
555    
556            // now add the rest as extra headers.
557            for (int i = 1; i < addresses.length; i++) {
558                Address address = addresses[i];
559                addHeader(name, address.toString());
560            }
561        }
562    
563    
564        void writeTo(OutputStream out, String[] ignore) throws IOException {
565            if (ignore == null) {
566                // write out all header lines with non-null values
567                for (int i = 0; i < headers.size(); i++) {
568                    InternetHeader header = (InternetHeader)headers.get(i);
569                    // we only include headers with real values, no placeholders
570                    if (header.getValue() != null) {
571                        header.writeTo(out);
572                    }
573                }
574            }
575            else {
576                // write out all matching header lines with non-null values
577                for (int i = 0; i < headers.size(); i++) {
578                    InternetHeader header = (InternetHeader)headers.get(i);
579                    // we only include headers with real values, no placeholders
580                    if (header.getValue() != null) {
581                        if (matchHeader(header.getName(), ignore)) {
582                            header.writeTo(out);
583                        }
584                    }
585                }
586            }
587        }
588    
589        protected static final class InternetHeader extends Header {
590    
591            public InternetHeader(String h) {
592                // initialize with null values, which we'll update once we parse the string
593                super("", "");
594                int separator = h.indexOf(':');
595                // no separator, then we take this as a name with a null string value.
596                if (separator == -1) {
597                    name = h.trim();
598                }
599                else {
600                    name = h.substring(0, separator);
601                    // step past the separator.  Now we need to remove any leading white space characters.
602                    separator++;
603    
604                    while (separator < h.length()) {
605                        char ch = h.charAt(separator);
606                        if (ch != ' ' && ch != '\t' && ch != '\r' && ch != '\n') {
607                            break;
608                        }
609                        separator++;
610                    }
611    
612                    value = h.substring(separator);
613                }
614            }
615    
616            public InternetHeader(String name, String value) {
617                super(name, value);
618            }
619    
620    
621            /**
622             * Package scope method for setting the header value.
623             *
624             * @param value  The new header value.
625             */
626            void setValue(String value) {
627                this.value = value;
628            }
629    
630            /**
631             * Package scope method for extending a header value.
632             *
633             * @param value  The appended header value.
634             */
635            void appendValue(String value) {
636                if (this.value == null) {
637                    this.value = value;
638                }
639                else {
640                    this.value = this.value + "\r\n" + value;
641                }
642            }
643    
644            void writeTo(OutputStream out) throws IOException {
645                out.write(name.getBytes());
646                out.write(':');
647                out.write(' ');
648                out.write(value.getBytes());
649                out.write('\r');
650                out.write('\n');
651            }
652        }
653    
654        private static class HeaderLineEnumeration implements Enumeration {
655            private Enumeration headers;
656    
657            public HeaderLineEnumeration(Enumeration headers) {
658                this.headers = headers;
659            }
660    
661            public boolean hasMoreElements() {
662                return headers.hasMoreElements();
663            }
664    
665            public Object nextElement() {
666                Header h = (Header) headers.nextElement();
667                return h.getName() + ": " + h.getValue();
668            }
669        }
670    }