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 }