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
020package org.apache.james.mime4j.field;
021
022import java.util.Arrays;
023import java.util.Collections;
024import java.util.Date;
025import java.util.HashMap;
026import java.util.Map;
027import java.util.TimeZone;
028import java.util.regex.Pattern;
029
030import org.apache.james.mime4j.codec.DecodeMonitor;
031import org.apache.james.mime4j.codec.EncoderUtil;
032import org.apache.james.mime4j.dom.FieldParser;
033import org.apache.james.mime4j.dom.address.Address;
034import org.apache.james.mime4j.dom.address.Mailbox;
035import org.apache.james.mime4j.dom.field.AddressListField;
036import org.apache.james.mime4j.dom.field.ContentDispositionField;
037import org.apache.james.mime4j.dom.field.ContentTransferEncodingField;
038import org.apache.james.mime4j.dom.field.ContentTypeField;
039import org.apache.james.mime4j.dom.field.DateTimeField;
040import org.apache.james.mime4j.dom.field.FieldName;
041import org.apache.james.mime4j.dom.field.MailboxField;
042import org.apache.james.mime4j.dom.field.MailboxListField;
043import org.apache.james.mime4j.dom.field.ParsedField;
044import org.apache.james.mime4j.dom.field.UnstructuredField;
045import org.apache.james.mime4j.field.address.AddressFormatter;
046import org.apache.james.mime4j.stream.Field;
047import org.apache.james.mime4j.stream.RawField;
048import org.apache.james.mime4j.util.MimeUtil;
049
050/**
051 * Factory for concrete {@link Field} instances.
052 */
053public 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>&quot;text/plain&quot;</code> or
082     *            <code>&quot;application/octet-stream&quot;</code>).
083     * @param parameters
084     *            map containing content-type parameters such as
085     *            <code>&quot;boundary&quot;</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>&quot;7-bit&quot;</code>
114     *            or <code>&quot;quoted-printable&quot;</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>&quot;inline&quot;</code>
145     *            or <code>&quot;attachment&quot;</code>).
146     * @param parameters
147     *            map containing disposition parameters such as
148     *            <code>&quot;filename&quot;</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>&quot;inline&quot;</code>
177     *            or <code>&quot;attachment&quot;</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>&quot;inline&quot;</code>
194     *            or <code>&quot;attachment&quot;</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>&quot;inline&quot;</code>
214     *            or <code>&quot;attachment&quot;</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}