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 }