View Javadoc

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