View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *  http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package javax.mail.internet;
21  
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.OutputStream;
25  import java.util.ArrayList;
26  import java.util.Arrays;
27  import java.util.Collections;
28  import java.util.Enumeration;
29  import java.util.HashSet;
30  import java.util.Iterator;
31  import java.util.LinkedHashMap;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Set;
35  
36  import javax.mail.Address;
37  import javax.mail.Header;
38  import javax.mail.MessagingException;
39  
40  /**
41   * Class that represents the RFC822 headers associated with a message.
42   *
43   * @version $Rev: 702234 $ $Date: 2008-10-06 15:25:41 -0400 (Mon, 06 Oct 2008) $
44   */
45  public class InternetHeaders {
46      // the list of headers (to preserve order);
47      protected List headers = new ArrayList();
48  
49      private transient String lastHeaderName;
50  
51      /**
52       * Create an empty InternetHeaders
53       */
54      public InternetHeaders() {
55          // these are created in the preferred order of the headers.
56          addHeader("Return-Path", null);
57          addHeader("Received", null);
58          addHeader("Resent-Date", null);
59          addHeader("Resent-From", null);
60          addHeader("Resent-Sender", null);
61          addHeader("Resent-To", null);
62          addHeader("Resent-Cc", null);
63          addHeader("Resent-Bcc", null);
64          addHeader("Resent-Message-Id", null);
65          addHeader("Date", null);
66          addHeader("From", null);
67          addHeader("Sender", null);
68          addHeader("Reply-To", null);
69          addHeader("To", null);
70          addHeader("Cc", null);
71          addHeader("Bcc", null);
72          addHeader("Message-Id", null);
73          addHeader("In-Reply-To", null);
74          addHeader("References", null);
75          addHeader("Subject", null);
76          addHeader("Comments", null);
77          addHeader("Keywords", null);
78          addHeader("Errors-To", null);
79          addHeader("MIME-Version", null);
80          addHeader("Content-Type", null);
81          addHeader("Content-Transfer-Encoding", null);
82          addHeader("Content-MD5", null);
83          // the following is a special marker used to identify new header insertion points.
84          addHeader(":", null);
85          addHeader("Content-Length", null);
86          addHeader("Status", null);
87      }
88  
89      /**
90       * Create a new InternetHeaders initialized by reading headers from the
91       * stream.
92       *
93       * @param in
94       *            the RFC822 input stream to load from
95       * @throws MessagingException
96       *             if there is a problem pasring the stream
97       */
98      public InternetHeaders(InputStream in) throws MessagingException {
99          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 }