001 /**
002 *
003 * Copyright 2003-2006 The Apache Software Foundation
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018 package javax.mail.internet;
019
020 import java.io.IOException;
021 import java.io.InputStream;
022 import java.io.OutputStream;
023 import java.util.ArrayList;
024 import java.util.Arrays;
025 import java.util.Collections;
026 import java.util.Enumeration;
027 import java.util.HashSet;
028 import java.util.Iterator;
029 import java.util.LinkedHashMap;
030 import java.util.List;
031 import java.util.Map;
032 import java.util.Set;
033
034 import javax.mail.Address;
035 import javax.mail.Header;
036 import javax.mail.MessagingException;
037
038 /**
039 * Class that represents the RFC822 headers associated with a message.
040 *
041 * @version $Rev: 421852 $ $Date: 2006-07-14 03:02:19 -0700 (Fri, 14 Jul 2006) $
042 */
043 public class InternetHeaders {
044 // the list of headers (to preserve order);
045 protected List headers = new ArrayList();
046
047 private transient String lastHeaderName;
048
049 /**
050 * Create an empty InternetHeaders
051 */
052 public InternetHeaders() {
053 // these are created in the preferred order of the headers.
054 addHeader("Return-path", null);
055 addHeader("Received", null);
056 addHeader("Message-ID", null);
057 addHeader("Resent-Date", null);
058 addHeader("Date", null);
059 addHeader("Resent-From", null);
060 addHeader("From", null);
061 addHeader("Reply-To", null);
062 addHeader("Sender", null);
063 addHeader("To", null);
064 addHeader("Subject", null);
065 addHeader("Cc", null);
066 addHeader("In-Reply-To", null);
067 addHeader("Resent-Message-Id", null);
068 addHeader("Errors-To", null);
069 addHeader("MIME-Version", null);
070 addHeader("Content-Type", null);
071 addHeader("Content-Transfer-Encoding", null);
072 addHeader("Content-MD5", null);
073 // the following is a special marker used to identify new header insertion points.
074 addHeader(":", null);
075 addHeader("Content-Length", null);
076 addHeader("Status", null);
077 }
078
079 /**
080 * Create a new InternetHeaders initialized by reading headers from the
081 * stream.
082 *
083 * @param in
084 * the RFC822 input stream to load from
085 * @throws MessagingException
086 * if there is a problem pasring the stream
087 */
088 public InternetHeaders(InputStream in) throws MessagingException {
089 load(in);
090 }
091
092 /**
093 * Read and parse the supplied stream and add all headers to the current
094 * set.
095 *
096 * @param in
097 * the RFC822 input stream to load from
098 * @throws MessagingException
099 * 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 }