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