/**************************************************************** * Licensed to the Apache Software Foundation (ASF) under one * * or more contributor license agreements. See the NOTICE file * * distributed with this work for additional information * * regarding copyright ownership. The ASF licenses this file * * to you under the Apache License, Version 2.0 (the * * "License"); you may not use this file except in compliance * * with the License. You may obtain a copy of the License at * * * * http://www.apache.org/licenses/LICENSE-2.0 * * * * Unless required by applicable law or agreed to in writing, * * software distributed under the License is distributed on an * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * * KIND, either express or implied. See the License for the * * specific language governing permissions and limitations * * under the License. * ****************************************************************/ package org.apache.james.mime4j.field; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.TimeZone; import java.util.regex.Pattern; import org.apache.james.mime4j.codec.EncoderUtil; import org.apache.james.mime4j.field.address.Address; import org.apache.james.mime4j.field.address.Mailbox; import org.apache.james.mime4j.parser.Field; import org.apache.james.mime4j.util.ByteSequence; import org.apache.james.mime4j.util.ContentUtil; import org.apache.james.mime4j.util.MimeUtil; /** * Factory for concrete {@link Field} instances. */ public class Fields { private static final Pattern FIELD_NAME_PATTERN = Pattern .compile("[\\x21-\\x39\\x3b-\\x7e]+"); private Fields() { } /** * Creates a <i>Content-Type</i> field from the specified raw field value. * The specified string gets folded into a multiple-line representation if * necessary but is otherwise taken as is. * * @param contentType * raw content type containing a MIME type and optional * parameters. * @return the newly created <i>Content-Type</i> field. */ public static ContentTypeField contentType(String contentType) { return parse(ContentTypeField.PARSER, FieldName.CONTENT_TYPE, contentType); } /** * Creates a <i>Content-Type</i> field from the specified MIME type and * parameters. * * @param mimeType * a MIME type (such as <code>"text/plain"</code> or * <code>"application/octet-stream"</code>). * @param parameters * map containing content-type parameters such as * <code>"boundary"</code>. * @return the newly created <i>Content-Type</i> field. */ public static ContentTypeField contentType(String mimeType, Map<String, String> parameters) { if (!isValidMimeType(mimeType)) throw new IllegalArgumentException(); if (parameters == null || parameters.isEmpty()) { return parse(ContentTypeField.PARSER, FieldName.CONTENT_TYPE, mimeType); } else { StringBuilder sb = new StringBuilder(mimeType); for (Map.Entry<String, String> entry : parameters.entrySet()) { sb.append("; "); sb.append(EncoderUtil.encodeHeaderParameter(entry.getKey(), entry.getValue())); } String contentType = sb.toString(); return contentType(contentType); } } /** * Creates a <i>Content-Transfer-Encoding</i> field from the specified raw * field value. * * @param contentTransferEncoding * an encoding mechanism such as <code>"7-bit"</code> * or <code>"quoted-printable"</code>. * @return the newly created <i>Content-Transfer-Encoding</i> field. */ public static ContentTransferEncodingField contentTransferEncoding( String contentTransferEncoding) { return parse(ContentTransferEncodingField.PARSER, FieldName.CONTENT_TRANSFER_ENCODING, contentTransferEncoding); } /** * Creates a <i>Content-Disposition</i> field from the specified raw field * value. The specified string gets folded into a multiple-line * representation if necessary but is otherwise taken as is. * * @param contentDisposition * raw content disposition containing a disposition type and * optional parameters. * @return the newly created <i>Content-Disposition</i> field. */ public static ContentDispositionField contentDisposition( String contentDisposition) { return parse(ContentDispositionField.PARSER, FieldName.CONTENT_DISPOSITION, contentDisposition); } /** * Creates a <i>Content-Disposition</i> field from the specified * disposition type and parameters. * * @param dispositionType * a disposition type (usually <code>"inline"</code> * or <code>"attachment"</code>). * @param parameters * map containing disposition parameters such as * <code>"filename"</code>. * @return the newly created <i>Content-Disposition</i> field. */ public static ContentDispositionField contentDisposition( String dispositionType, Map<String, String> parameters) { if (!isValidDispositionType(dispositionType)) throw new IllegalArgumentException(); if (parameters == null || parameters.isEmpty()) { return parse(ContentDispositionField.PARSER, FieldName.CONTENT_DISPOSITION, dispositionType); } else { StringBuilder sb = new StringBuilder(dispositionType); for (Map.Entry<String, String> entry : parameters.entrySet()) { sb.append("; "); sb.append(EncoderUtil.encodeHeaderParameter(entry.getKey(), entry.getValue())); } String contentDisposition = sb.toString(); return contentDisposition(contentDisposition); } } /** * Creates a <i>Content-Disposition</i> field from the specified * disposition type and filename. * * @param dispositionType * a disposition type (usually <code>"inline"</code> * or <code>"attachment"</code>). * @param filename * filename parameter value or <code>null</code> if the * parameter should not be included. * @return the newly created <i>Content-Disposition</i> field. */ public static ContentDispositionField contentDisposition( String dispositionType, String filename) { return contentDisposition(dispositionType, filename, -1, null, null, null); } /** * Creates a <i>Content-Disposition</i> field from the specified values. * * @param dispositionType * a disposition type (usually <code>"inline"</code> * or <code>"attachment"</code>). * @param filename * filename parameter value or <code>null</code> if the * parameter should not be included. * @param size * size parameter value or <code>-1</code> if the parameter * should not be included. * @return the newly created <i>Content-Disposition</i> field. */ public static ContentDispositionField contentDisposition( String dispositionType, String filename, long size) { return contentDisposition(dispositionType, filename, size, null, null, null); } /** * Creates a <i>Content-Disposition</i> field from the specified values. * * @param dispositionType * a disposition type (usually <code>"inline"</code> * or <code>"attachment"</code>). * @param filename * filename parameter value or <code>null</code> if the * parameter should not be included. * @param size * size parameter value or <code>-1</code> if the parameter * should not be included. * @param creationDate * creation-date parameter value or <code>null</code> if the * parameter should not be included. * @param modificationDate * modification-date parameter value or <code>null</code> if * the parameter should not be included. * @param readDate * read-date parameter value or <code>null</code> if the * parameter should not be included. * @return the newly created <i>Content-Disposition</i> field. */ public static ContentDispositionField contentDisposition( String dispositionType, String filename, long size, Date creationDate, Date modificationDate, Date readDate) { Map<String, String> parameters = new HashMap<String, String>(); if (filename != null) { parameters.put(ContentDispositionField.PARAM_FILENAME, filename); } if (size >= 0) { parameters.put(ContentDispositionField.PARAM_SIZE, Long .toString(size)); } if (creationDate != null) { parameters.put(ContentDispositionField.PARAM_CREATION_DATE, MimeUtil.formatDate(creationDate, null)); } if (modificationDate != null) { parameters.put(ContentDispositionField.PARAM_MODIFICATION_DATE, MimeUtil.formatDate(modificationDate, null)); } if (readDate != null) { parameters.put(ContentDispositionField.PARAM_READ_DATE, MimeUtil .formatDate(readDate, null)); } return contentDisposition(dispositionType, parameters); } /** * Creates a <i>Date</i> field from the specified <code>Date</code> * value. The default time zone of the host is used to format the date. * * @param date * date value for the header field. * @return the newly created <i>Date</i> field. */ public static DateTimeField date(Date date) { return date0(FieldName.DATE, date, null); } /** * Creates a date field from the specified field name and <code>Date</code> * value. The default time zone of the host is used to format the date. * * @param fieldName * a field name such as <code>Date</code> or * <code>Resent-Date</code>. * @param date * date value for the header field. * @return the newly created date field. */ public static DateTimeField date(String fieldName, Date date) { checkValidFieldName(fieldName); return date0(fieldName, date, null); } /** * Creates a date field from the specified field name, <code>Date</code> * and <code>TimeZone</code> values. * * @param fieldName * a field name such as <code>Date</code> or * <code>Resent-Date</code>. * @param date * date value for the header field. * @param zone * the time zone to be used for formatting the date. * @return the newly created date field. */ public static DateTimeField date(String fieldName, Date date, TimeZone zone) { checkValidFieldName(fieldName); return date0(fieldName, date, zone); } /** * Creates a <i>Message-ID</i> field for the specified host name. * * @param hostname * host name to be included in the message ID or * <code>null</code> if no host name should be included. * @return the newly created <i>Message-ID</i> field. */ public static Field messageId(String hostname) { String fieldValue = MimeUtil.createUniqueMessageId(hostname); return parse(UnstructuredField.PARSER, FieldName.MESSAGE_ID, fieldValue); } /** * Creates a <i>Subject</i> field from the specified string value. The * specified string may contain non-ASCII characters. * * @param subject * the subject string. * @return the newly created <i>Subject</i> field. */ public static UnstructuredField subject(String subject) { int usedCharacters = FieldName.SUBJECT.length() + 2; String fieldValue = EncoderUtil.encodeIfNecessary(subject, EncoderUtil.Usage.TEXT_TOKEN, usedCharacters); return parse(UnstructuredField.PARSER, FieldName.SUBJECT, fieldValue); } /** * Creates a <i>Sender</i> field for the specified mailbox address. * * @param mailbox * address to be included in the field. * @return the newly created <i>Sender</i> field. */ public static MailboxField sender(Mailbox mailbox) { return mailbox0(FieldName.SENDER, mailbox); } /** * Creates a <i>From</i> field for the specified mailbox address. * * @param mailbox * address to be included in the field. * @return the newly created <i>From</i> field. */ public static MailboxListField from(Mailbox mailbox) { return mailboxList0(FieldName.FROM, Collections.singleton(mailbox)); } /** * Creates a <i>From</i> field for the specified mailbox addresses. * * @param mailboxes * addresses to be included in the field. * @return the newly created <i>From</i> field. */ public static MailboxListField from(Mailbox... mailboxes) { return mailboxList0(FieldName.FROM, Arrays.asList(mailboxes)); } /** * Creates a <i>From</i> field for the specified mailbox addresses. * * @param mailboxes * addresses to be included in the field. * @return the newly created <i>From</i> field. */ public static MailboxListField from(Iterable<Mailbox> mailboxes) { return mailboxList0(FieldName.FROM, mailboxes); } /** * Creates a <i>To</i> field for the specified mailbox or group address. * * @param address * mailbox or group address to be included in the field. * @return the newly created <i>To</i> field. */ public static AddressListField to(Address address) { return addressList0(FieldName.TO, Collections.singleton(address)); } /** * Creates a <i>To</i> field for the specified mailbox or group addresses. * * @param addresses * mailbox or group addresses to be included in the field. * @return the newly created <i>To</i> field. */ public static AddressListField to(Address... addresses) { return addressList0(FieldName.TO, Arrays.asList(addresses)); } /** * Creates a <i>To</i> field for the specified mailbox or group addresses. * * @param addresses * mailbox or group addresses to be included in the field. * @return the newly created <i>To</i> field. */ public static AddressListField to(Iterable<Address> addresses) { return addressList0(FieldName.TO, addresses); } /** * Creates a <i>Cc</i> field for the specified mailbox or group address. * * @param address * mailbox or group address to be included in the field. * @return the newly created <i>Cc</i> field. */ public static AddressListField cc(Address address) { return addressList0(FieldName.CC, Collections.singleton(address)); } /** * Creates a <i>Cc</i> field for the specified mailbox or group addresses. * * @param addresses * mailbox or group addresses to be included in the field. * @return the newly created <i>Cc</i> field. */ public static AddressListField cc(Address... addresses) { return addressList0(FieldName.CC, Arrays.asList(addresses)); } /** * Creates a <i>Cc</i> field for the specified mailbox or group addresses. * * @param addresses * mailbox or group addresses to be included in the field. * @return the newly created <i>Cc</i> field. */ public static AddressListField cc(Iterable<Address> addresses) { return addressList0(FieldName.CC, addresses); } /** * Creates a <i>Bcc</i> field for the specified mailbox or group address. * * @param address * mailbox or group address to be included in the field. * @return the newly created <i>Bcc</i> field. */ public static AddressListField bcc(Address address) { return addressList0(FieldName.BCC, Collections.singleton(address)); } /** * Creates a <i>Bcc</i> field for the specified mailbox or group addresses. * * @param addresses * mailbox or group addresses to be included in the field. * @return the newly created <i>Bcc</i> field. */ public static AddressListField bcc(Address... addresses) { return addressList0(FieldName.BCC, Arrays.asList(addresses)); } /** * Creates a <i>Bcc</i> field for the specified mailbox or group addresses. * * @param addresses * mailbox or group addresses to be included in the field. * @return the newly created <i>Bcc</i> field. */ public static AddressListField bcc(Iterable<Address> addresses) { return addressList0(FieldName.BCC, addresses); } /** * Creates a <i>Reply-To</i> field for the specified mailbox or group * address. * * @param address * mailbox or group address to be included in the field. * @return the newly created <i>Reply-To</i> field. */ public static AddressListField replyTo(Address address) { return addressList0(FieldName.REPLY_TO, Collections.singleton(address)); } /** * Creates a <i>Reply-To</i> field for the specified mailbox or group * addresses. * * @param addresses * mailbox or group addresses to be included in the field. * @return the newly created <i>Reply-To</i> field. */ public static AddressListField replyTo(Address... addresses) { return addressList0(FieldName.REPLY_TO, Arrays.asList(addresses)); } /** * Creates a <i>Reply-To</i> field for the specified mailbox or group * addresses. * * @param addresses * mailbox or group addresses to be included in the field. * @return the newly created <i>Reply-To</i> field. */ public static AddressListField replyTo(Iterable<Address> addresses) { return addressList0(FieldName.REPLY_TO, addresses); } /** * Creates a mailbox field from the specified field name and mailbox * address. Valid field names are <code>Sender</code> and * <code>Resent-Sender</code>. * * @param fieldName * the name of the mailbox field (<code>Sender</code> or * <code>Resent-Sender</code>). * @param mailbox * mailbox address for the field value. * @return the newly created mailbox field. */ public static MailboxField mailbox(String fieldName, Mailbox mailbox) { checkValidFieldName(fieldName); return mailbox0(fieldName, mailbox); } /** * Creates a mailbox-list field from the specified field name and mailbox * addresses. Valid field names are <code>From</code> and * <code>Resent-From</code>. * * @param fieldName * the name of the mailbox field (<code>From</code> or * <code>Resent-From</code>). * @param mailboxes * mailbox addresses for the field value. * @return the newly created mailbox-list field. */ public static MailboxListField mailboxList(String fieldName, Iterable<Mailbox> mailboxes) { checkValidFieldName(fieldName); return mailboxList0(fieldName, mailboxes); } /** * Creates an address-list field from the specified field name and mailbox * or group addresses. Valid field names are <code>To</code>, * <code>Cc</code>, <code>Bcc</code>, <code>Reply-To</code>, * <code>Resent-To</code>, <code>Resent-Cc</code> and * <code>Resent-Bcc</code>. * * @param fieldName * the name of the mailbox field (<code>To</code>, * <code>Cc</code>, <code>Bcc</code>, <code>Reply-To</code>, * <code>Resent-To</code>, <code>Resent-Cc</code> or * <code>Resent-Bcc</code>). * @param addresses * mailbox or group addresses for the field value. * @return the newly created address-list field. */ public static AddressListField addressList(String fieldName, Iterable<Address> addresses) { checkValidFieldName(fieldName); return addressList0(fieldName, addresses); } private static DateTimeField date0(String fieldName, Date date, TimeZone zone) { final String formattedDate = MimeUtil.formatDate(date, zone); return parse(DateTimeField.PARSER, fieldName, formattedDate); } private static MailboxField mailbox0(String fieldName, Mailbox mailbox) { String fieldValue = encodeAddresses(Collections.singleton(mailbox)); return parse(MailboxField.PARSER, fieldName, fieldValue); } private static MailboxListField mailboxList0(String fieldName, Iterable<Mailbox> mailboxes) { String fieldValue = encodeAddresses(mailboxes); return parse(MailboxListField.PARSER, fieldName, fieldValue); } private static AddressListField addressList0(String fieldName, Iterable<Address> addresses) { String fieldValue = encodeAddresses(addresses); return parse(AddressListField.PARSER, fieldName, fieldValue); } private static void checkValidFieldName(String fieldName) { if (!FIELD_NAME_PATTERN.matcher(fieldName).matches()) throw new IllegalArgumentException("Invalid field name"); } private static boolean isValidMimeType(String mimeType) { if (mimeType == null) return false; int idx = mimeType.indexOf('/'); if (idx == -1) return false; String type = mimeType.substring(0, idx); String subType = mimeType.substring(idx + 1); return EncoderUtil.isToken(type) && EncoderUtil.isToken(subType); } private static boolean isValidDispositionType(String dispositionType) { if (dispositionType == null) return false; return EncoderUtil.isToken(dispositionType); } private static <F extends Field> F parse(FieldParser parser, String fieldName, String fieldBody) { String rawStr = MimeUtil.fold(fieldName + ": " + fieldBody, 0); ByteSequence raw = ContentUtil.encode(rawStr); Field field = parser.parse(fieldName, fieldBody, raw); @SuppressWarnings("unchecked") F f = (F) field; return f; } private static String encodeAddresses(Iterable<? extends Address> addresses) { StringBuilder sb = new StringBuilder(); for (Address address : addresses) { if (sb.length() > 0) { sb.append(", "); } sb.append(address.getEncodedString()); } return sb.toString(); } }