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: 517148 $ $Date: 2007-03-12 05:31:41 -0400 (Mon, 12 Mar 2007) $
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("Message-ID", null);
59          addHeader("Resent-Date", null);
60          addHeader("Date", null);
61          addHeader("Resent-From", null);
62          addHeader("From", null);
63          addHeader("Reply-To", null);
64          addHeader("Sender", null);
65          addHeader("To", null);
66          addHeader("Subject", null);
67          addHeader("Cc", null);
68          addHeader("In-Reply-To", null);
69          addHeader("Resent-Message-Id", null);
70          addHeader("Errors-To", null);
71          addHeader("MIME-Version", null);
72          addHeader("Content-Type", null);
73          addHeader("Content-Transfer-Encoding", null);
74          addHeader("Content-MD5", null);
75          // the following is a special marker used to identify new header insertion points.
76          addHeader(":", null);
77          addHeader("Content-Length", null);
78          addHeader("Status", null);
79      }
80  
81      /**
82       * Create a new InternetHeaders initialized by reading headers from the
83       * stream.
84       *
85       * @param in
86       *            the RFC822 input stream to load from
87       * @throws MessagingException
88       *             if there is a problem pasring the stream
89       */
90      public InternetHeaders(InputStream in) throws MessagingException {
91          load(in);
92      }
93  
94      /**
95       * Read and parse the supplied stream and add all headers to the current
96       * set.
97       *
98       * @param in
99       *            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 }