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