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 org.apache.james.mime4j.field;
021
022 import java.util.Arrays;
023 import java.util.Collections;
024 import java.util.Date;
025 import java.util.HashMap;
026 import java.util.Map;
027 import java.util.TimeZone;
028 import java.util.regex.Pattern;
029
030 import org.apache.james.mime4j.codec.DecodeMonitor;
031 import org.apache.james.mime4j.codec.EncoderUtil;
032 import org.apache.james.mime4j.dom.FieldParser;
033 import org.apache.james.mime4j.dom.address.Address;
034 import org.apache.james.mime4j.dom.address.Mailbox;
035 import org.apache.james.mime4j.dom.field.AddressListField;
036 import org.apache.james.mime4j.dom.field.ContentDispositionField;
037 import org.apache.james.mime4j.dom.field.ContentTransferEncodingField;
038 import org.apache.james.mime4j.dom.field.ContentTypeField;
039 import org.apache.james.mime4j.dom.field.DateTimeField;
040 import org.apache.james.mime4j.dom.field.FieldName;
041 import org.apache.james.mime4j.dom.field.MailboxField;
042 import org.apache.james.mime4j.dom.field.MailboxListField;
043 import org.apache.james.mime4j.dom.field.ParsedField;
044 import org.apache.james.mime4j.dom.field.UnstructuredField;
045 import org.apache.james.mime4j.field.address.AddressFormatter;
046 import org.apache.james.mime4j.stream.Field;
047 import org.apache.james.mime4j.stream.RawField;
048 import org.apache.james.mime4j.util.MimeUtil;
049
050 /**
051 * Factory for concrete {@link Field} instances.
052 */
053 public class Fields {
054
055 private static final Pattern FIELD_NAME_PATTERN = Pattern
056 .compile("[\\x21-\\x39\\x3b-\\x7e]+");
057
058 private Fields() {
059 }
060
061 /**
062 * Creates a <i>Content-Type</i> field from the specified raw field value.
063 * The specified string gets folded into a multiple-line representation if
064 * necessary but is otherwise taken as is.
065 *
066 * @param contentType
067 * raw content type containing a MIME type and optional
068 * parameters.
069 * @return the newly created <i>Content-Type</i> field.
070 */
071 public static ContentTypeField contentType(String contentType) {
072 return parse(ContentTypeFieldImpl.PARSER, FieldName.CONTENT_TYPE,
073 contentType);
074 }
075
076 /**
077 * Creates a <i>Content-Type</i> field from the specified MIME type and
078 * parameters.
079 *
080 * @param mimeType
081 * a MIME type (such as <code>"text/plain"</code> or
082 * <code>"application/octet-stream"</code>).
083 * @param parameters
084 * map containing content-type parameters such as
085 * <code>"boundary"</code>.
086 * @return the newly created <i>Content-Type</i> field.
087 */
088 public static ContentTypeField contentType(String mimeType,
089 Map<String, String> parameters) {
090 if (!isValidMimeType(mimeType))
091 throw new IllegalArgumentException();
092
093 if (parameters == null || parameters.isEmpty()) {
094 return parse(ContentTypeFieldImpl.PARSER, FieldName.CONTENT_TYPE,
095 mimeType);
096 } else {
097 StringBuilder sb = new StringBuilder(mimeType);
098 for (Map.Entry<String, String> entry : parameters.entrySet()) {
099 sb.append("; ");
100 sb.append(EncoderUtil.encodeHeaderParameter(entry.getKey(),
101 entry.getValue()));
102 }
103 String contentType = sb.toString();
104 return contentType(contentType);
105 }
106 }
107
108 /**
109 * Creates a <i>Content-Transfer-Encoding</i> field from the specified raw
110 * field value.
111 *
112 * @param contentTransferEncoding
113 * an encoding mechanism such as <code>"7-bit"</code>
114 * or <code>"quoted-printable"</code>.
115 * @return the newly created <i>Content-Transfer-Encoding</i> field.
116 */
117 public static ContentTransferEncodingField contentTransferEncoding(
118 String contentTransferEncoding) {
119 return parse(ContentTransferEncodingFieldImpl.PARSER,
120 FieldName.CONTENT_TRANSFER_ENCODING, contentTransferEncoding);
121 }
122
123 /**
124 * Creates a <i>Content-Disposition</i> field from the specified raw field
125 * value. The specified string gets folded into a multiple-line
126 * representation if necessary but is otherwise taken as is.
127 *
128 * @param contentDisposition
129 * raw content disposition containing a disposition type and
130 * optional parameters.
131 * @return the newly created <i>Content-Disposition</i> field.
132 */
133 public static ContentDispositionField contentDisposition(
134 String contentDisposition) {
135 return parse(ContentDispositionFieldImpl.PARSER,
136 FieldName.CONTENT_DISPOSITION, contentDisposition);
137 }
138
139 /**
140 * Creates a <i>Content-Disposition</i> field from the specified
141 * disposition type and parameters.
142 *
143 * @param dispositionType
144 * a disposition type (usually <code>"inline"</code>
145 * or <code>"attachment"</code>).
146 * @param parameters
147 * map containing disposition parameters such as
148 * <code>"filename"</code>.
149 * @return the newly created <i>Content-Disposition</i> field.
150 */
151 public static ContentDispositionField contentDisposition(
152 String dispositionType, Map<String, String> parameters) {
153 if (!isValidDispositionType(dispositionType))
154 throw new IllegalArgumentException();
155
156 if (parameters == null || parameters.isEmpty()) {
157 return parse(ContentDispositionFieldImpl.PARSER,
158 FieldName.CONTENT_DISPOSITION, dispositionType);
159 } else {
160 StringBuilder sb = new StringBuilder(dispositionType);
161 for (Map.Entry<String, String> entry : parameters.entrySet()) {
162 sb.append("; ");
163 sb.append(EncoderUtil.encodeHeaderParameter(entry.getKey(),
164 entry.getValue()));
165 }
166 String contentDisposition = sb.toString();
167 return contentDisposition(contentDisposition);
168 }
169 }
170
171 /**
172 * Creates a <i>Content-Disposition</i> field from the specified
173 * disposition type and filename.
174 *
175 * @param dispositionType
176 * a disposition type (usually <code>"inline"</code>
177 * or <code>"attachment"</code>).
178 * @param filename
179 * filename parameter value or <code>null</code> if the
180 * parameter should not be included.
181 * @return the newly created <i>Content-Disposition</i> field.
182 */
183 public static ContentDispositionField contentDisposition(
184 String dispositionType, String filename) {
185 return contentDisposition(dispositionType, filename, -1, null, null,
186 null);
187 }
188
189 /**
190 * Creates a <i>Content-Disposition</i> field from the specified values.
191 *
192 * @param dispositionType
193 * a disposition type (usually <code>"inline"</code>
194 * or <code>"attachment"</code>).
195 * @param filename
196 * filename parameter value or <code>null</code> if the
197 * parameter should not be included.
198 * @param size
199 * size parameter value or <code>-1</code> if the parameter
200 * should not be included.
201 * @return the newly created <i>Content-Disposition</i> field.
202 */
203 public static ContentDispositionField contentDisposition(
204 String dispositionType, String filename, long size) {
205 return contentDisposition(dispositionType, filename, size, null, null,
206 null);
207 }
208
209 /**
210 * Creates a <i>Content-Disposition</i> field from the specified values.
211 *
212 * @param dispositionType
213 * a disposition type (usually <code>"inline"</code>
214 * or <code>"attachment"</code>).
215 * @param filename
216 * filename parameter value or <code>null</code> if the
217 * parameter should not be included.
218 * @param size
219 * size parameter value or <code>-1</code> if the parameter
220 * should not be included.
221 * @param creationDate
222 * creation-date parameter value or <code>null</code> if the
223 * parameter should not be included.
224 * @param modificationDate
225 * modification-date parameter value or <code>null</code> if
226 * the parameter should not be included.
227 * @param readDate
228 * read-date parameter value or <code>null</code> if the
229 * parameter should not be included.
230 * @return the newly created <i>Content-Disposition</i> field.
231 */
232 public static ContentDispositionField contentDisposition(
233 String dispositionType, String filename, long size,
234 Date creationDate, Date modificationDate, Date readDate) {
235 Map<String, String> parameters = new HashMap<String, String>();
236 if (filename != null) {
237 parameters.put(ContentDispositionFieldImpl.PARAM_FILENAME, filename);
238 }
239 if (size >= 0) {
240 parameters.put(ContentDispositionFieldImpl.PARAM_SIZE, Long
241 .toString(size));
242 }
243 if (creationDate != null) {
244 parameters.put(ContentDispositionFieldImpl.PARAM_CREATION_DATE,
245 MimeUtil.formatDate(creationDate, null));
246 }
247 if (modificationDate != null) {
248 parameters.put(ContentDispositionFieldImpl.PARAM_MODIFICATION_DATE,
249 MimeUtil.formatDate(modificationDate, null));
250 }
251 if (readDate != null) {
252 parameters.put(ContentDispositionFieldImpl.PARAM_READ_DATE, MimeUtil
253 .formatDate(readDate, null));
254 }
255 return contentDisposition(dispositionType, parameters);
256 }
257
258 /**
259 * Creates a <i>Date</i> field from the specified <code>Date</code>
260 * value. The default time zone of the host is used to format the date.
261 *
262 * @param date
263 * date value for the header field.
264 * @return the newly created <i>Date</i> field.
265 */
266 public static DateTimeField date(Date date) {
267 return date0(FieldName.DATE, date, null);
268 }
269
270 /**
271 * Creates a date field from the specified field name and <code>Date</code>
272 * value. The default time zone of the host is used to format the date.
273 *
274 * @param fieldName
275 * a field name such as <code>Date</code> or
276 * <code>Resent-Date</code>.
277 * @param date
278 * date value for the header field.
279 * @return the newly created date field.
280 */
281 public static DateTimeField date(String fieldName, Date date) {
282 checkValidFieldName(fieldName);
283 return date0(fieldName, date, null);
284 }
285
286 /**
287 * Creates a date field from the specified field name, <code>Date</code>
288 * and <code>TimeZone</code> values.
289 *
290 * @param fieldName
291 * a field name such as <code>Date</code> or
292 * <code>Resent-Date</code>.
293 * @param date
294 * date value for the header field.
295 * @param zone
296 * the time zone to be used for formatting the date.
297 * @return the newly created date field.
298 */
299 public static DateTimeField date(String fieldName, Date date, TimeZone zone) {
300 checkValidFieldName(fieldName);
301 return date0(fieldName, date, zone);
302 }
303
304 /**
305 * Creates a <i>Message-ID</i> field for the specified host name.
306 *
307 * @param hostname
308 * host name to be included in the message ID or
309 * <code>null</code> if no host name should be included.
310 * @return the newly created <i>Message-ID</i> field.
311 */
312 public static UnstructuredField messageId(String hostname) {
313 String fieldValue = MimeUtil.createUniqueMessageId(hostname);
314 return parse(UnstructuredFieldImpl.PARSER, FieldName.MESSAGE_ID, fieldValue);
315 }
316
317 /**
318 * Creates a <i>Subject</i> field from the specified string value. The
319 * specified string may contain non-ASCII characters.
320 *
321 * @param subject
322 * the subject string.
323 * @return the newly created <i>Subject</i> field.
324 */
325 public static UnstructuredField subject(String subject) {
326 int usedCharacters = FieldName.SUBJECT.length() + 2;
327 String fieldValue = EncoderUtil.encodeIfNecessary(subject,
328 EncoderUtil.Usage.TEXT_TOKEN, usedCharacters);
329
330 return parse(UnstructuredFieldImpl.PARSER, FieldName.SUBJECT, fieldValue);
331 }
332
333 /**
334 * Creates a <i>Sender</i> field for the specified mailbox address.
335 *
336 * @param mailbox
337 * address to be included in the field.
338 * @return the newly created <i>Sender</i> field.
339 */
340 public static MailboxField sender(Mailbox mailbox) {
341 return mailbox0(FieldName.SENDER, mailbox);
342 }
343
344 /**
345 * Creates a <i>From</i> field for the specified mailbox address.
346 *
347 * @param mailbox
348 * address to be included in the field.
349 * @return the newly created <i>From</i> field.
350 */
351 public static MailboxListField from(Mailbox mailbox) {
352 return mailboxList0(FieldName.FROM, Collections.singleton(mailbox));
353 }
354
355 /**
356 * Creates a <i>From</i> field for the specified mailbox addresses.
357 *
358 * @param mailboxes
359 * addresses to be included in the field.
360 * @return the newly created <i>From</i> field.
361 */
362 public static MailboxListField from(Mailbox... mailboxes) {
363 return mailboxList0(FieldName.FROM, Arrays.asList(mailboxes));
364 }
365
366 /**
367 * Creates a <i>From</i> field for the specified mailbox addresses.
368 *
369 * @param mailboxes
370 * addresses to be included in the field.
371 * @return the newly created <i>From</i> field.
372 */
373 public static MailboxListField from(Iterable<Mailbox> mailboxes) {
374 return mailboxList0(FieldName.FROM, mailboxes);
375 }
376
377 /**
378 * Creates a <i>To</i> field for the specified mailbox or group address.
379 *
380 * @param address
381 * mailbox or group address to be included in the field.
382 * @return the newly created <i>To</i> field.
383 */
384 public static AddressListField to(Address address) {
385 return addressList0(FieldName.TO, Collections.singleton(address));
386 }
387
388 /**
389 * Creates a <i>To</i> field for the specified mailbox or group addresses.
390 *
391 * @param addresses
392 * mailbox or group addresses to be included in the field.
393 * @return the newly created <i>To</i> field.
394 */
395 public static AddressListField to(Address... addresses) {
396 return addressList0(FieldName.TO, Arrays.asList(addresses));
397 }
398
399 /**
400 * Creates a <i>To</i> field for the specified mailbox or group addresses.
401 *
402 * @param addresses
403 * mailbox or group addresses to be included in the field.
404 * @return the newly created <i>To</i> field.
405 */
406 public static AddressListField to(Iterable<Address> addresses) {
407 return addressList0(FieldName.TO, addresses);
408 }
409
410 /**
411 * Creates a <i>Cc</i> field for the specified mailbox or group address.
412 *
413 * @param address
414 * mailbox or group address to be included in the field.
415 * @return the newly created <i>Cc</i> field.
416 */
417 public static AddressListField cc(Address address) {
418 return addressList0(FieldName.CC, Collections.singleton(address));
419 }
420
421 /**
422 * Creates a <i>Cc</i> field for the specified mailbox or group addresses.
423 *
424 * @param addresses
425 * mailbox or group addresses to be included in the field.
426 * @return the newly created <i>Cc</i> field.
427 */
428 public static AddressListField cc(Address... addresses) {
429 return addressList0(FieldName.CC, Arrays.asList(addresses));
430 }
431
432 /**
433 * Creates a <i>Cc</i> field for the specified mailbox or group addresses.
434 *
435 * @param addresses
436 * mailbox or group addresses to be included in the field.
437 * @return the newly created <i>Cc</i> field.
438 */
439 public static AddressListField cc(Iterable<Address> addresses) {
440 return addressList0(FieldName.CC, addresses);
441 }
442
443 /**
444 * Creates a <i>Bcc</i> field for the specified mailbox or group address.
445 *
446 * @param address
447 * mailbox or group address to be included in the field.
448 * @return the newly created <i>Bcc</i> field.
449 */
450 public static AddressListField bcc(Address address) {
451 return addressList0(FieldName.BCC, Collections.singleton(address));
452 }
453
454 /**
455 * Creates a <i>Bcc</i> field for the specified mailbox or group addresses.
456 *
457 * @param addresses
458 * mailbox or group addresses to be included in the field.
459 * @return the newly created <i>Bcc</i> field.
460 */
461 public static AddressListField bcc(Address... addresses) {
462 return addressList0(FieldName.BCC, Arrays.asList(addresses));
463 }
464
465 /**
466 * Creates a <i>Bcc</i> field for the specified mailbox or group addresses.
467 *
468 * @param addresses
469 * mailbox or group addresses to be included in the field.
470 * @return the newly created <i>Bcc</i> field.
471 */
472 public static AddressListField bcc(Iterable<Address> addresses) {
473 return addressList0(FieldName.BCC, addresses);
474 }
475
476 /**
477 * Creates a <i>Reply-To</i> field for the specified mailbox or group
478 * address.
479 *
480 * @param address
481 * mailbox or group address to be included in the field.
482 * @return the newly created <i>Reply-To</i> field.
483 */
484 public static AddressListField replyTo(Address address) {
485 return addressList0(FieldName.REPLY_TO, Collections.singleton(address));
486 }
487
488 /**
489 * Creates a <i>Reply-To</i> field for the specified mailbox or group
490 * addresses.
491 *
492 * @param addresses
493 * mailbox or group addresses to be included in the field.
494 * @return the newly created <i>Reply-To</i> field.
495 */
496 public static AddressListField replyTo(Address... addresses) {
497 return addressList0(FieldName.REPLY_TO, Arrays.asList(addresses));
498 }
499
500 /**
501 * Creates a <i>Reply-To</i> field for the specified mailbox or group
502 * addresses.
503 *
504 * @param addresses
505 * mailbox or group addresses to be included in the field.
506 * @return the newly created <i>Reply-To</i> field.
507 */
508 public static AddressListField replyTo(Iterable<Address> addresses) {
509 return addressList0(FieldName.REPLY_TO, addresses);
510 }
511
512 /**
513 * Creates a mailbox field from the specified field name and mailbox
514 * address. Valid field names are <code>Sender</code> and
515 * <code>Resent-Sender</code>.
516 *
517 * @param fieldName
518 * the name of the mailbox field (<code>Sender</code> or
519 * <code>Resent-Sender</code>).
520 * @param mailbox
521 * mailbox address for the field value.
522 * @return the newly created mailbox field.
523 */
524 public static MailboxField mailbox(String fieldName, Mailbox mailbox) {
525 checkValidFieldName(fieldName);
526 return mailbox0(fieldName, mailbox);
527 }
528
529 /**
530 * Creates a mailbox-list field from the specified field name and mailbox
531 * addresses. Valid field names are <code>From</code> and
532 * <code>Resent-From</code>.
533 *
534 * @param fieldName
535 * the name of the mailbox field (<code>From</code> or
536 * <code>Resent-From</code>).
537 * @param mailboxes
538 * mailbox addresses for the field value.
539 * @return the newly created mailbox-list field.
540 */
541 public static MailboxListField mailboxList(String fieldName,
542 Iterable<Mailbox> mailboxes) {
543 checkValidFieldName(fieldName);
544 return mailboxList0(fieldName, mailboxes);
545 }
546
547 /**
548 * Creates an address-list field from the specified field name and mailbox
549 * or group addresses. Valid field names are <code>To</code>,
550 * <code>Cc</code>, <code>Bcc</code>, <code>Reply-To</code>,
551 * <code>Resent-To</code>, <code>Resent-Cc</code> and
552 * <code>Resent-Bcc</code>.
553 *
554 * @param fieldName
555 * the name of the mailbox field (<code>To</code>,
556 * <code>Cc</code>, <code>Bcc</code>, <code>Reply-To</code>,
557 * <code>Resent-To</code>, <code>Resent-Cc</code> or
558 * <code>Resent-Bcc</code>).
559 * @param addresses
560 * mailbox or group addresses for the field value.
561 * @return the newly created address-list field.
562 */
563 public static AddressListField addressList(String fieldName,
564 Iterable<? extends Address> addresses) {
565 checkValidFieldName(fieldName);
566 return addressList0(fieldName, addresses);
567 }
568
569 private static DateTimeField date0(String fieldName, Date date,
570 TimeZone zone) {
571 final String formattedDate = MimeUtil.formatDate(date, zone);
572 return parse(DateTimeFieldImpl.PARSER, fieldName, formattedDate);
573 }
574
575 private static MailboxField mailbox0(String fieldName, Mailbox mailbox) {
576 String fieldValue = encodeAddresses(Collections.singleton(mailbox));
577 return parse(MailboxFieldImpl.PARSER, fieldName, fieldValue);
578 }
579
580 private static MailboxListField mailboxList0(String fieldName,
581 Iterable<Mailbox> mailboxes) {
582 String fieldValue = encodeAddresses(mailboxes);
583 return parse(MailboxListFieldImpl.PARSER, fieldName, fieldValue);
584 }
585
586 private static AddressListField addressList0(String fieldName,
587 Iterable<? extends Address> addresses) {
588 String fieldValue = encodeAddresses(addresses);
589 return parse(AddressListFieldImpl.PARSER, fieldName, fieldValue);
590 }
591
592 private static void checkValidFieldName(String fieldName) {
593 if (!FIELD_NAME_PATTERN.matcher(fieldName).matches())
594 throw new IllegalArgumentException("Invalid field name");
595 }
596
597 private static boolean isValidMimeType(String mimeType) {
598 if (mimeType == null)
599 return false;
600
601 int idx = mimeType.indexOf('/');
602 if (idx == -1)
603 return false;
604
605 String type = mimeType.substring(0, idx);
606 String subType = mimeType.substring(idx + 1);
607 return EncoderUtil.isToken(type) && EncoderUtil.isToken(subType);
608 }
609
610 private static boolean isValidDispositionType(String dispositionType) {
611 if (dispositionType == null)
612 return false;
613
614 return EncoderUtil.isToken(dispositionType);
615 }
616
617 private static <F extends ParsedField> F parse(FieldParser<F> parser,
618 String fieldName, String fieldBody) {
619 RawField rawField = new RawField(fieldName, fieldBody);
620 return parser.parse(rawField, DecodeMonitor.SILENT);
621 }
622
623 private static String encodeAddresses(Iterable<? extends Address> addresses) {
624 StringBuilder sb = new StringBuilder();
625
626 for (Address address : addresses) {
627 if (sb.length() > 0) {
628 sb.append(", ");
629 }
630 AddressFormatter.DEFAULT.encode(sb, address);
631 }
632 return sb.toString();
633 }
634
635 }