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: 688710 $ $Date: 2008-08-25 08:53:14 -0400 (Mon, 25 Aug 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 ew need to replace this 583 if (addresses.length == 0) { 584 removeHeader(name); 585 } 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 * Write out the set of headers, except for any 600 * headers specified in the optional ignore list. 601 * 602 * @param out The output stream. 603 * @param ignore The optional ignore list. 604 * 605 * @exception IOException 606 */ 607 void writeTo(OutputStream out, String[] ignore) throws IOException { 608 if (ignore == null) { 609 // write out all header lines with non-null values 610 for (int i = 0; i < headers.size(); i++) { 611 InternetHeader header = (InternetHeader)headers.get(i); 612 // we only include headers with real values, no placeholders 613 if (header.getValue() != null) { 614 header.writeTo(out); 615 } 616 } 617 } 618 else { 619 // write out all matching header lines with non-null values 620 for (int i = 0; i < headers.size(); i++) { 621 InternetHeader header = (InternetHeader)headers.get(i); 622 // we only include headers with real values, no placeholders 623 if (header.getValue() != null) { 624 if (!matchHeader(header.getName(), ignore)) { 625 header.writeTo(out); 626 } 627 } 628 } 629 } 630 } 631 632 protected static final class InternetHeader extends Header { 633 634 public InternetHeader(String h) { 635 // initialize with null values, which we'll update once we parse the string 636 super("", ""); 637 int separator = h.indexOf(':'); 638 // no separator, then we take this as a name with a null string value. 639 if (separator == -1) { 640 name = h.trim(); 641 } 642 else { 643 name = h.substring(0, separator); 644 // step past the separator. Now we need to remove any leading white space characters. 645 separator++; 646 647 while (separator < h.length()) { 648 char ch = h.charAt(separator); 649 if (ch != ' ' && ch != '\t' && ch != '\r' && ch != '\n') { 650 break; 651 } 652 separator++; 653 } 654 655 value = h.substring(separator); 656 } 657 } 658 659 public InternetHeader(String name, String value) { 660 super(name, value); 661 } 662 663 664 /** 665 * Package scope method for setting the header value. 666 * 667 * @param value The new header value. 668 */ 669 void setValue(String value) { 670 this.value = value; 671 } 672 673 674 /** 675 * Package scope method for setting the name value. 676 * 677 * @param name The new header name 678 */ 679 void setName(String name) { 680 this.name = name; 681 } 682 683 /** 684 * Package scope method for extending a header value. 685 * 686 * @param value The appended header value. 687 */ 688 void appendValue(String value) { 689 if (this.value == null) { 690 this.value = value; 691 } 692 else { 693 this.value = this.value + "\r\n" + value; 694 } 695 } 696 697 void writeTo(OutputStream out) throws IOException { 698 out.write(name.getBytes()); 699 out.write(':'); 700 out.write(' '); 701 out.write(value.getBytes()); 702 out.write('\r'); 703 out.write('\n'); 704 } 705 } 706 707 private static class HeaderLineEnumeration implements Enumeration { 708 private Enumeration headers; 709 710 public HeaderLineEnumeration(Enumeration headers) { 711 this.headers = headers; 712 } 713 714 public boolean hasMoreElements() { 715 return headers.hasMoreElements(); 716 } 717 718 public Object nextElement() { 719 Header h = (Header) headers.nextElement(); 720 return h.getName() + ": " + h.getValue(); 721 } 722 } 723 }