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>&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    }