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