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