diff --git a/app/build.gradle b/app/build.gradle index b8f48fe663..a5fa660cb3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -296,7 +296,7 @@ dependencies { // https://javaee.github.io/javamail/ // https://projects.eclipse.org/projects/ee4j.javamail // https://mvnrepository.com/artifact/com.sun.mail - implementation "com.sun.mail:android-mail:$javamail_version" + //implementation "com.sun.mail:android-mail:$javamail_version" implementation "com.sun.mail:android-activation:$javamail_version" // https://jsoup.org/news/ diff --git a/app/src/main/java/com/sun/mail/auth/MD4.java b/app/src/main/java/com/sun/mail/auth/MD4.java new file mode 100644 index 0000000000..566de43f1e --- /dev/null +++ b/app/src/main/java/com/sun/mail/auth/MD4.java @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2005, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/* + * Copied from OpenJDK with permission. + */ + +package com.sun.mail.auth; + +import java.security.*; + +//import static sun.security.provider.ByteArrayAccess.*; + +/** + * The MD4 class is used to compute an MD4 message digest over a given + * buffer of bytes. It is an implementation of the RSA Data Security Inc + * MD4 algorithim as described in internet RFC 1320. + * + * @author Andreas Sterbenz + * @author Bill Shannon (adapted for Jakarta Mail) + */ +public final class MD4 { + + // state of this object + private final int[] state; + // temporary buffer, used by implCompress() + private final int[] x; + + // size of the input to the compression function in bytes + private static final int blockSize = 64; + + // buffer to store partial blocks, blockSize bytes large + private final byte[] buffer = new byte[blockSize]; + // offset into buffer + private int bufOfs; + + // number of bytes processed so far. + // also used as a flag to indicate reset status + // -1: need to call engineReset() before next call to update() + // 0: is already reset + private long bytesProcessed; + + // rotation constants + private static final int S11 = 3; + private static final int S12 = 7; + private static final int S13 = 11; + private static final int S14 = 19; + private static final int S21 = 3; + private static final int S22 = 5; + private static final int S23 = 9; + private static final int S24 = 13; + private static final int S31 = 3; + private static final int S32 = 9; + private static final int S33 = 11; + private static final int S34 = 15; + + private static final byte[] padding; + + static { + padding = new byte[136]; + padding[0] = (byte)0x80; + } + + /** + * Standard constructor, creates a new MD4 instance. + */ + public MD4() { + state = new int[4]; + x = new int[16]; + implReset(); + } + + /** + * Compute and return the message digest of the input byte array. + * + * @param in the input byte array + * @return the message digest byte array + */ + public byte[] digest(byte[] in) { + implReset(); + engineUpdate(in, 0, in.length); + byte[] out = new byte[16]; + implDigest(out, 0); + return out; + } + + /** + * Reset the state of this object. + */ + private void implReset() { + // Load magic initialization constants. + state[0] = 0x67452301; + state[1] = 0xefcdab89; + state[2] = 0x98badcfe; + state[3] = 0x10325476; + bufOfs = 0; + bytesProcessed = 0; + } + + /** + * Perform the final computations, any buffered bytes are added + * to the digest, the count is added to the digest, and the resulting + * digest is stored. + */ + private void implDigest(byte[] out, int ofs) { + long bitsProcessed = bytesProcessed << 3; + + int index = (int)bytesProcessed & 0x3f; + int padLen = (index < 56) ? (56 - index) : (120 - index); + engineUpdate(padding, 0, padLen); + + //i2bLittle4((int)bitsProcessed, buffer, 56); + //i2bLittle4((int)(bitsProcessed >>> 32), buffer, 60); + buffer[56] = (byte)bitsProcessed; + buffer[57] = (byte)(bitsProcessed>>8); + buffer[58] = (byte)(bitsProcessed>>16); + buffer[59] = (byte)(bitsProcessed>>24); + buffer[60] = (byte)(bitsProcessed>>32); + buffer[61] = (byte)(bitsProcessed>>40); + buffer[62] = (byte)(bitsProcessed>>48); + buffer[63] = (byte)(bitsProcessed>>56); + implCompress(buffer, 0); + + //i2bLittle(state, 0, out, ofs, 16); + for (int i = 0; i < state.length; i++) { + int x = state[i]; + out[ofs++] = (byte)x; + out[ofs++] = (byte)(x>>8); + out[ofs++] = (byte)(x>>16); + out[ofs++] = (byte)(x>>24); + } + } + + private void engineUpdate(byte[] b, int ofs, int len) { + if (len == 0) { + return; + } + if ((ofs < 0) || (len < 0) || (ofs > b.length - len)) { + throw new ArrayIndexOutOfBoundsException(); + } + if (bytesProcessed < 0) { + implReset(); + } + bytesProcessed += len; + // if buffer is not empty, we need to fill it before proceeding + if (bufOfs != 0) { + int n = Math.min(len, blockSize - bufOfs); + System.arraycopy(b, ofs, buffer, bufOfs, n); + bufOfs += n; + ofs += n; + len -= n; + if (bufOfs >= blockSize) { + // compress completed block now + implCompress(buffer, 0); + bufOfs = 0; + } + } + // compress complete blocks + while (len >= blockSize) { + implCompress(b, ofs); + len -= blockSize; + ofs += blockSize; + } + // copy remainder to buffer + if (len > 0) { + System.arraycopy(b, ofs, buffer, 0, len); + bufOfs = len; + } + } + + private static int FF(int a, int b, int c, int d, int x, int s) { + a += ((b & c) | ((~b) & d)) + x; + return ((a << s) | (a >>> (32 - s))); + } + + private static int GG(int a, int b, int c, int d, int x, int s) { + a += ((b & c) | (b & d) | (c & d)) + x + 0x5a827999; + return ((a << s) | (a >>> (32 - s))); + } + + private static int HH(int a, int b, int c, int d, int x, int s) { + a += ((b ^ c) ^ d) + x + 0x6ed9eba1; + return ((a << s) | (a >>> (32 - s))); + } + + /** + * This is where the functions come together as the generic MD4 + * transformation operation. It consumes 64 + * bytes from the buffer, beginning at the specified offset. + */ + private void implCompress(byte[] buf, int ofs) { + //b2iLittle64(buf, ofs, x); + for (int xfs = 0; xfs < x.length; xfs++) { + x[xfs] = (buf[ofs] & 0xff) | ((buf[ofs+1] & 0xff) << 8) | + ((buf[ofs+2] & 0xff) << 16) | ((buf[ofs+3] & 0xff) << 24); + ofs += 4; + } + + int a = state[0]; + int b = state[1]; + int c = state[2]; + int d = state[3]; + + /* Round 1 */ + a = FF (a, b, c, d, x[ 0], S11); /* 1 */ + d = FF (d, a, b, c, x[ 1], S12); /* 2 */ + c = FF (c, d, a, b, x[ 2], S13); /* 3 */ + b = FF (b, c, d, a, x[ 3], S14); /* 4 */ + a = FF (a, b, c, d, x[ 4], S11); /* 5 */ + d = FF (d, a, b, c, x[ 5], S12); /* 6 */ + c = FF (c, d, a, b, x[ 6], S13); /* 7 */ + b = FF (b, c, d, a, x[ 7], S14); /* 8 */ + a = FF (a, b, c, d, x[ 8], S11); /* 9 */ + d = FF (d, a, b, c, x[ 9], S12); /* 10 */ + c = FF (c, d, a, b, x[10], S13); /* 11 */ + b = FF (b, c, d, a, x[11], S14); /* 12 */ + a = FF (a, b, c, d, x[12], S11); /* 13 */ + d = FF (d, a, b, c, x[13], S12); /* 14 */ + c = FF (c, d, a, b, x[14], S13); /* 15 */ + b = FF (b, c, d, a, x[15], S14); /* 16 */ + + /* Round 2 */ + a = GG (a, b, c, d, x[ 0], S21); /* 17 */ + d = GG (d, a, b, c, x[ 4], S22); /* 18 */ + c = GG (c, d, a, b, x[ 8], S23); /* 19 */ + b = GG (b, c, d, a, x[12], S24); /* 20 */ + a = GG (a, b, c, d, x[ 1], S21); /* 21 */ + d = GG (d, a, b, c, x[ 5], S22); /* 22 */ + c = GG (c, d, a, b, x[ 9], S23); /* 23 */ + b = GG (b, c, d, a, x[13], S24); /* 24 */ + a = GG (a, b, c, d, x[ 2], S21); /* 25 */ + d = GG (d, a, b, c, x[ 6], S22); /* 26 */ + c = GG (c, d, a, b, x[10], S23); /* 27 */ + b = GG (b, c, d, a, x[14], S24); /* 28 */ + a = GG (a, b, c, d, x[ 3], S21); /* 29 */ + d = GG (d, a, b, c, x[ 7], S22); /* 30 */ + c = GG (c, d, a, b, x[11], S23); /* 31 */ + b = GG (b, c, d, a, x[15], S24); /* 32 */ + + /* Round 3 */ + a = HH (a, b, c, d, x[ 0], S31); /* 33 */ + d = HH (d, a, b, c, x[ 8], S32); /* 34 */ + c = HH (c, d, a, b, x[ 4], S33); /* 35 */ + b = HH (b, c, d, a, x[12], S34); /* 36 */ + a = HH (a, b, c, d, x[ 2], S31); /* 37 */ + d = HH (d, a, b, c, x[10], S32); /* 38 */ + c = HH (c, d, a, b, x[ 6], S33); /* 39 */ + b = HH (b, c, d, a, x[14], S34); /* 40 */ + a = HH (a, b, c, d, x[ 1], S31); /* 41 */ + d = HH (d, a, b, c, x[ 9], S32); /* 42 */ + c = HH (c, d, a, b, x[ 5], S33); /* 43 */ + b = HH (b, c, d, a, x[13], S34); /* 44 */ + a = HH (a, b, c, d, x[ 3], S31); /* 45 */ + d = HH (d, a, b, c, x[11], S32); /* 46 */ + c = HH (c, d, a, b, x[ 7], S33); /* 47 */ + b = HH (b, c, d, a, x[15], S34); /* 48 */ + + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + } +} diff --git a/app/src/main/java/com/sun/mail/auth/Ntlm.java b/app/src/main/java/com/sun/mail/auth/Ntlm.java new file mode 100644 index 0000000000..882776fcca --- /dev/null +++ b/app/src/main/java/com/sun/mail/auth/Ntlm.java @@ -0,0 +1,498 @@ +/* + * Copyright (c) 2005, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/* + * Copied from OpenJDK with permission. + */ + +package com.sun.mail.auth; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.io.PrintStream; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Locale; +import java.util.Random; +import java.util.logging.Level; +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; +import javax.crypto.spec.SecretKeySpec; + +import com.sun.mail.util.BASE64DecoderStream; +import com.sun.mail.util.BASE64EncoderStream; +import com.sun.mail.util.MailLogger; + + +/** + * NTLMAuthentication: + * + * @author Michael McMahon + * @author Bill Shannon (adapted for Jakarta Mail) + */ +public class Ntlm { + + private byte[] type1; + private byte[] type3; + + private SecretKeyFactory fac; + private Cipher cipher; + private MD4 md4; + private String hostname; + private String ntdomain; + private String username; + private String password; + + private Mac hmac; + + private MailLogger logger; + + // NTLM flags, as defined in Microsoft NTLM spec + // https://msdn.microsoft.com/en-us/library/cc236621.aspx + private static final int NTLMSSP_NEGOTIATE_UNICODE = 0x00000001; + private static final int NTLMSSP_NEGOTIATE_OEM = 0x00000002; + private static final int NTLMSSP_REQUEST_TARGET = 0x00000004; + private static final int NTLMSSP_NEGOTIATE_SIGN = 0x00000010; + private static final int NTLMSSP_NEGOTIATE_SEAL = 0x00000020; + private static final int NTLMSSP_NEGOTIATE_DATAGRAM = 0x00000040; + private static final int NTLMSSP_NEGOTIATE_LM_KEY = 0x00000080; + private static final int NTLMSSP_NEGOTIATE_NTLM = 0x00000200; + private static final int NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED = 0x00001000; + private static final int NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED = 0x00002000; + private static final int NTLMSSP_NEGOTIATE_ALWAYS_SIGN = 0x00008000; + private static final int NTLMSSP_TARGET_TYPE_DOMAIN = 0x00010000; + private static final int NTLMSSP_TARGET_TYPE_SERVER = 0x00020000; + private static final int NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY = 0x00080000; + private static final int NTLMSSP_NEGOTIATE_IDENTIFY = 0x00100000; + private static final int NTLMSSP_REQUEST_NON_NT_SESSION_KEY = 0x00400000; + private static final int NTLMSSP_NEGOTIATE_TARGET_INFO = 0x00800000; + private static final int NTLMSSP_NEGOTIATE_VERSION = 0x02000000; + private static final int NTLMSSP_NEGOTIATE_128 = 0x20000000; + private static final int NTLMSSP_NEGOTIATE_KEY_EXCH = 0x40000000; + private static final int NTLMSSP_NEGOTIATE_56 = 0x80000000; + + private static final byte RESPONSERVERSION = 1; + private static final byte HIRESPONSERVERSION = 1; + private static final byte[] Z6 = new byte[] { 0, 0, 0, 0, 0, 0 }; + private static final byte[] Z4 = new byte[] { 0, 0, 0, 0 }; + + private void init0() { + type1 = new byte[256]; // hopefully large enough + type3 = new byte[512]; // ditto + System.arraycopy(new byte[] {'N','T','L','M','S','S','P',0,1}, 0, + type1, 0, 9); + System.arraycopy(new byte[] {'N','T','L','M','S','S','P',0,3}, 0, + type3, 0, 9); + + try { + fac = SecretKeyFactory.getInstance("DES"); + cipher = Cipher.getInstance("DES/ECB/NoPadding"); + md4 = new MD4(); + } catch (NoSuchPaddingException e) { + assert false; + } catch (NoSuchAlgorithmException e) { + assert false; + } + }; + + /** + * Create an NTLM authenticator. + * Username may be specified as domain\\username in the Authenticator. + * If this notation is not used, then the domain will be taken + * from the ntdomain parameter. + * + * @param ntdomain the NT domain + * @param hostname the host name + * @param username the user name + * @param password the password + * @param logger the MailLogger + */ + public Ntlm(String ntdomain, String hostname, String username, + String password, MailLogger logger) { + int i = hostname.indexOf('.'); + if (i != -1) { + hostname = hostname.substring(0, i); + } + i = username.indexOf('\\'); + if (i != -1) { + ntdomain = username.substring(0, i).toUpperCase(Locale.ENGLISH); + username = username.substring(i+1); + } else if (ntdomain == null) { + ntdomain = ""; + } + this.ntdomain = ntdomain; + this.hostname = hostname; + this.username = username; + this.password = password; + this.logger = logger.getLogger(this.getClass(), "DEBUG NTLM"); + init0(); + } + + private void copybytes(byte[] dest, int destpos, String src, String enc) { + try { + byte[] x = src.getBytes(enc); + System.arraycopy(x, 0, dest, destpos, x.length); + } catch (UnsupportedEncodingException e) { + assert false; + } + } + + // for compatibility, just in case + public String generateType1Msg(int flags) { + return generateType1Msg(flags, false); + } + + public String generateType1Msg(int flags, boolean v2) { + int dlen = ntdomain.length(); + int type1flags = + NTLMSSP_NEGOTIATE_UNICODE | + NTLMSSP_NEGOTIATE_OEM | + NTLMSSP_NEGOTIATE_NTLM | + NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED | + NTLMSSP_NEGOTIATE_ALWAYS_SIGN | + flags; + if (dlen != 0) + type1flags |= NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED; + if (v2) + type1flags |= NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY; + writeInt(type1, 12, type1flags); + type1[28] = (byte) 0x20; // host name offset + writeShort(type1, 16, dlen); + writeShort(type1, 18, dlen); + + int hlen = hostname.length(); + writeShort(type1, 24, hlen); + writeShort(type1, 26, hlen); + + copybytes(type1, 32, hostname, "iso-8859-1"); + copybytes(type1, hlen+32, ntdomain, "iso-8859-1"); + writeInt(type1, 20, hlen+32); + + byte[] msg = new byte[32 + hlen + dlen]; + System.arraycopy(type1, 0, msg, 0, 32 + hlen + dlen); + if (logger.isLoggable(Level.FINE)) + logger.fine("type 1 message: " + toHex(msg)); + + String result = null; + try { + result = new String(BASE64EncoderStream.encode(msg), "iso-8859-1"); + } catch (UnsupportedEncodingException e) { + assert false; + } + return result; + } + + /** + * Convert a 7 byte array to an 8 byte array (for a des key with parity). + * Input starts at offset off. + */ + private byte[] makeDesKey(byte[] input, int off) { + int[] in = new int[input.length]; + for (int i = 0; i < in.length; i++) { + in[i] = input[i] < 0 ? input[i] + 256: input[i]; + } + byte[] out = new byte[8]; + out[0] = (byte)in[off+0]; + out[1] = (byte)(((in[off+0] << 7) & 0xFF) | (in[off+1] >> 1)); + out[2] = (byte)(((in[off+1] << 6) & 0xFF) | (in[off+2] >> 2)); + out[3] = (byte)(((in[off+2] << 5) & 0xFF) | (in[off+3] >> 3)); + out[4] = (byte)(((in[off+3] << 4) & 0xFF) | (in[off+4] >> 4)); + out[5] = (byte)(((in[off+4] << 3) & 0xFF) | (in[off+5] >> 5)); + out[6] = (byte)(((in[off+5] << 2) & 0xFF) | (in[off+6] >> 6)); + out[7] = (byte)((in[off+6] << 1) & 0xFF); + return out; + } + + /** + * Compute hash-based message authentication code for NTLMv2. + */ + private byte[] hmacMD5(byte[] key, byte[] text) { + try { + if (hmac == null) + hmac = Mac.getInstance("HmacMD5"); + } catch (NoSuchAlgorithmException ex) { + throw new AssertionError(); + } + try { + byte[] nk = new byte[16]; + System.arraycopy(key, 0, nk, 0, key.length > 16 ? 16 : key.length); + SecretKeySpec skey = new SecretKeySpec(nk, "HmacMD5"); + hmac.init(skey); + return hmac.doFinal(text); + } catch (InvalidKeyException ex) { + assert false; + } catch (RuntimeException e) { + assert false; + } + return null; + } + + private byte[] calcLMHash() throws GeneralSecurityException { + byte[] magic = {0x4b, 0x47, 0x53, 0x21, 0x40, 0x23, 0x24, 0x25}; + byte[] pwb = null; + try { + pwb = password.toUpperCase(Locale.ENGLISH).getBytes("iso-8859-1"); + } catch (UnsupportedEncodingException ex) { + // should never happen + assert false; + } + byte[] pwb1 = new byte[14]; + int len = password.length(); + if (len > 14) + len = 14; + System.arraycopy(pwb, 0, pwb1, 0, len); /* Zero padded */ + + DESKeySpec dks1 = new DESKeySpec(makeDesKey(pwb1, 0)); + DESKeySpec dks2 = new DESKeySpec(makeDesKey(pwb1, 7)); + + SecretKey key1 = fac.generateSecret(dks1); + SecretKey key2 = fac.generateSecret(dks2); + cipher.init(Cipher.ENCRYPT_MODE, key1); + byte[] out1 = cipher.doFinal(magic, 0, 8); + cipher.init(Cipher.ENCRYPT_MODE, key2); + byte[] out2 = cipher.doFinal(magic, 0, 8); + + byte[] result = new byte [21]; + System.arraycopy(out1, 0, result, 0, 8); + System.arraycopy(out2, 0, result, 8, 8); + return result; + } + + private byte[] calcNTHash() throws GeneralSecurityException { + byte[] pw = null; + try { + pw = password.getBytes("UnicodeLittleUnmarked"); + } catch (UnsupportedEncodingException e) { + assert false; + } + byte[] out = md4.digest(pw); + byte[] result = new byte[21]; + System.arraycopy(out, 0, result, 0, 16); + return result; + } + + /* + * Key is a 21 byte array. Split it into 3 7 byte chunks, + * convert each to 8 byte DES keys, encrypt the text arg with + * each key and return the three results in a sequential []. + */ + private byte[] calcResponse(byte[] key, byte[] text) + throws GeneralSecurityException { + assert key.length == 21; + DESKeySpec dks1 = new DESKeySpec(makeDesKey(key, 0)); + DESKeySpec dks2 = new DESKeySpec(makeDesKey(key, 7)); + DESKeySpec dks3 = new DESKeySpec(makeDesKey(key, 14)); + SecretKey key1 = fac.generateSecret(dks1); + SecretKey key2 = fac.generateSecret(dks2); + SecretKey key3 = fac.generateSecret(dks3); + cipher.init(Cipher.ENCRYPT_MODE, key1); + byte[] out1 = cipher.doFinal(text, 0, 8); + cipher.init(Cipher.ENCRYPT_MODE, key2); + byte[] out2 = cipher.doFinal(text, 0, 8); + cipher.init(Cipher.ENCRYPT_MODE, key3); + byte[] out3 = cipher.doFinal(text, 0, 8); + byte[] result = new byte[24]; + System.arraycopy(out1, 0, result, 0, 8); + System.arraycopy(out2, 0, result, 8, 8); + System.arraycopy(out3, 0, result, 16, 8); + return result; + } + + /* + * Calculate the NTLMv2 response based on the nthash, additional data, + * and the original challenge. + */ + private byte[] calcV2Response(byte[] nthash, byte[] blob, byte[] challenge) + throws GeneralSecurityException { + byte[] txt = null; + try { + txt = (username.toUpperCase(Locale.ENGLISH) + ntdomain). + getBytes("UnicodeLittleUnmarked"); + } catch (UnsupportedEncodingException ex) { + // should never happen + assert false; + } + byte[] ntlmv2hash = hmacMD5(nthash, txt); + byte[] cb = new byte[blob.length + 8]; + System.arraycopy(challenge, 0, cb, 0, 8); + System.arraycopy(blob, 0, cb, 8, blob.length); + byte[] result = new byte[blob.length + 16]; + System.arraycopy(hmacMD5(ntlmv2hash, cb), 0, result, 0, 16); + System.arraycopy(blob, 0, result, 16, blob.length); + return result; + } + + public String generateType3Msg(String type2msg) { + try { + + /* First decode the type2 message to get the server challenge */ + /* challenge is located at type2[24] for 8 bytes */ + byte[] type2 = null; + try { + type2 = BASE64DecoderStream.decode(type2msg.getBytes("us-ascii")); + } catch (UnsupportedEncodingException ex) { + // should never happen + assert false; + } + if (logger.isLoggable(Level.FINE)) + logger.fine("type 2 message: " + toHex(type2)); + + byte[] challenge = new byte[8]; + System.arraycopy(type2, 24, challenge, 0, 8); + + int type3flags = + NTLMSSP_NEGOTIATE_UNICODE | + NTLMSSP_NEGOTIATE_NTLM | + NTLMSSP_NEGOTIATE_ALWAYS_SIGN; + + int ulen = username.length()*2; + writeShort(type3, 36, ulen); + writeShort(type3, 38, ulen); + int dlen = ntdomain.length()*2; + writeShort(type3, 28, dlen); + writeShort(type3, 30, dlen); + int hlen = hostname.length()*2; + writeShort(type3, 44, hlen); + writeShort(type3, 46, hlen); + + int l = 64; + copybytes(type3, l, ntdomain, "UnicodeLittleUnmarked"); + writeInt(type3, 32, l); + l += dlen; + copybytes(type3, l, username, "UnicodeLittleUnmarked"); + writeInt(type3, 40, l); + l += ulen; + copybytes(type3, l, hostname, "UnicodeLittleUnmarked"); + writeInt(type3, 48, l); + l += hlen; + + byte[] msg = null; + byte[] lmresponse = null; + byte[] ntresponse = null; + int flags = readInt(type2, 20); + + // did the server agree to NTLMv2? + if ((flags & NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY) != 0) { + // yes, create an NTLMv2 response + logger.fine("Using NTLMv2"); + type3flags |= NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY; + byte[] nonce = new byte[8]; + // XXX - allow user to specify Random instance via properties? + (new Random()).nextBytes(nonce); + byte[] nthash = calcNTHash(); + lmresponse = calcV2Response(nthash, nonce, challenge); + byte[] targetInfo = new byte[0]; + if ((flags & NTLMSSP_NEGOTIATE_TARGET_INFO) != 0) { + int tlen = readShort(type2, 40); + int toff = readInt(type2, 44); + targetInfo = new byte[tlen]; + System.arraycopy(type2, toff, targetInfo, 0, tlen); + } + byte[] blob = new byte[32 + targetInfo.length]; + blob[0] = RESPONSERVERSION; + blob[1] = HIRESPONSERVERSION; + System.arraycopy(Z6, 0, blob, 2, 6); + // convert time to NT format + long now = (System.currentTimeMillis() + 11644473600000L) * 10000L; + for (int i = 0; i < 8; i++) { + blob[8 + i] = (byte)(now & 0xff); + now >>= 8; + } + System.arraycopy(nonce, 0, blob, 16, 8); + System.arraycopy(Z4, 0, blob, 24, 4); + System.arraycopy(targetInfo, 0, blob, 28, targetInfo.length); + System.arraycopy(Z4, 0, blob, 28 + targetInfo.length, 4); + ntresponse = calcV2Response(nthash, blob, challenge); + } else { + byte[] lmhash = calcLMHash(); + lmresponse = calcResponse(lmhash, challenge); + byte[] nthash = calcNTHash(); + ntresponse = calcResponse(nthash, challenge); + } + System.arraycopy(lmresponse, 0, type3, l, lmresponse.length); + writeShort(type3, 12, lmresponse.length); + writeShort(type3, 14, lmresponse.length); + writeInt(type3, 16, l); + l += 24; + System.arraycopy(ntresponse, 0, type3, l, ntresponse.length); + writeShort(type3, 20, ntresponse.length); + writeShort(type3, 22, ntresponse.length); + writeInt(type3, 24, l); + l += ntresponse.length; + writeShort(type3, 56, l); + + msg = new byte[l]; + System.arraycopy(type3, 0, msg, 0, l); + + writeInt(type3, 60, type3flags); + + if (logger.isLoggable(Level.FINE)) + logger.fine("type 3 message: " + toHex(msg)); + + String result = null; + try { + result = new String(BASE64EncoderStream.encode(msg), "iso-8859-1"); + } catch (UnsupportedEncodingException e) { + assert false; + } + return result; + + } catch (GeneralSecurityException ex) { + // should never happen + logger.log(Level.FINE, "GeneralSecurityException", ex); + return ""; // will fail later + } + } + + private static int readShort(byte[] b, int off) { + return (((int)b[off]) & 0xff) | + ((((int)b[off+1]) & 0xff) << 8); + } + + private void writeShort(byte[] b, int off, int data) { + b[off] = (byte) (data & 0xff); + b[off+1] = (byte) ((data >> 8) & 0xff); + } + + private static int readInt(byte[] b, int off) { + return (((int)b[off]) & 0xff) | + ((((int)b[off+1]) & 0xff) << 8) | + ((((int)b[off+2]) & 0xff) << 16) | + ((((int)b[off+3]) & 0xff) << 24); + } + + private void writeInt(byte[] b, int off, int data) { + b[off] = (byte) (data & 0xff); + b[off+1] = (byte) ((data >> 8) & 0xff); + b[off+2] = (byte) ((data >> 16) & 0xff); + b[off+3] = (byte) ((data >> 24) & 0xff); + } + + private static char[] hex = + { '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F' }; + + private static String toHex(byte[] b) { + StringBuilder sb = new StringBuilder(b.length * 3); + for (int i = 0; i < b.length; i++) + sb.append(hex[(b[i]>>4)&0xF]).append(hex[b[i]&0xF]).append(' '); + return sb.toString(); + } +} diff --git a/app/src/main/java/com/sun/mail/auth/package.html b/app/src/main/java/com/sun/mail/auth/package.html new file mode 100644 index 0000000000..476f2ee06b --- /dev/null +++ b/app/src/main/java/com/sun/mail/auth/package.html @@ -0,0 +1,33 @@ + + + + + + +com.sun.mail.auth package + + + +

+This package includes internal authentication support classes and +SHOULD NOT BE USED DIRECTLY BY APPLICATIONS. +

+ + + diff --git a/app/src/main/java/com/sun/mail/handlers/handler_base.java b/app/src/main/java/com/sun/mail/handlers/handler_base.java new file mode 100644 index 0000000000..dae159fdd6 --- /dev/null +++ b/app/src/main/java/com/sun/mail/handlers/handler_base.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import java.io.IOException; +import javax.activation.*; + +/** + * Base class for other DataContentHandlers. + */ +public abstract class handler_base implements DataContentHandler { + + /** + * Return an array of ActivationDataFlavors that we support. + * Usually there will be only one. + * + * @return array of ActivationDataFlavors that we support + */ + protected abstract ActivationDataFlavor[] getDataFlavors(); + + /** + * Given the flavor that matched, return the appropriate type of object. + * Usually there's only one flavor so just call getContent. + * + * @param aFlavor the ActivationDataFlavor + * @param ds DataSource containing the data + * @return the object + * @exception IOException for errors reading the data + */ + protected Object getData(ActivationDataFlavor aFlavor, DataSource ds) + throws IOException { + return getContent(ds); + } + + /** + * Return the DataFlavors for this DataContentHandler. + * + * @return The DataFlavors + */ + public ActivationDataFlavor[] getTransferDataFlavors() { + return getDataFlavors().clone(); + } + + /** + * Return the Transfer Data of type DataFlavor from InputStream. + * + * @param df The DataFlavor + * @param ds The DataSource corresponding to the data + * @return the object + * @exception IOException for errors reading the data + */ + public Object getTransferData(ActivationDataFlavor df, DataSource ds) + throws IOException { + ActivationDataFlavor[] adf = getDataFlavors(); + for (int i = 0; i < adf.length; i++) { + // use ActivationDataFlavor.equals, which properly + // ignores Content-Type parameters in comparison + if (adf[i].equals(df)) + return getData(adf[i], ds); + } + return null; + } +} diff --git a/app/src/main/java/com/sun/mail/handlers/message_rfc822.java b/app/src/main/java/com/sun/mail/handlers/message_rfc822.java new file mode 100644 index 0000000000..a242e6b3e7 --- /dev/null +++ b/app/src/main/java/com/sun/mail/handlers/message_rfc822.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import java.io.*; +import java.util.Properties; +import javax.activation.*; +import javax.mail.*; +import javax.mail.internet.*; + + +/** + * @author Christopher Cotton + */ + + +public class message_rfc822 extends handler_base { + + private static ActivationDataFlavor[] ourDataFlavor = { + new ActivationDataFlavor(Message.class, "message/rfc822", "Message") + }; + + @Override + protected ActivationDataFlavor[] getDataFlavors() { + return ourDataFlavor; + } + + /** + * Return the content. + */ + @Override + public Object getContent(DataSource ds) throws IOException { + // create a new MimeMessage + try { + Session session; + if (ds instanceof MessageAware) { + MessageContext mc = ((MessageAware)ds).getMessageContext(); + session = mc.getSession(); + } else { + // Hopefully a rare case. Also hopefully the application + // has created a default Session that can just be returned + // here. If not, the one we create here is better than + // nothing, but overall not a really good answer. + session = Session.getDefaultInstance(new Properties(), null); + } + return new MimeMessage(session, ds.getInputStream()); + } catch (MessagingException me) { + IOException ioex = + new IOException("Exception creating MimeMessage in " + + "message/rfc822 DataContentHandler"); + ioex.initCause(me); + throw ioex; + } + } + + /** + * Write the object as a byte stream. + */ + @Override + public void writeTo(Object obj, String mimeType, OutputStream os) + throws IOException { + if (!(obj instanceof Message)) + throw new IOException("\"" + getDataFlavors()[0].getMimeType() + + "\" DataContentHandler requires Message object, " + + "was given object of type " + obj.getClass().toString() + + "; obj.cl " + obj.getClass().getClassLoader() + + ", Message.cl " + Message.class.getClassLoader()); + + // if the object is a message, we know how to write that out + Message m = (Message)obj; + try { + m.writeTo(os); + } catch (MessagingException me) { + IOException ioex = new IOException("Exception writing message"); + ioex.initCause(me); + throw ioex; + } + } +} diff --git a/app/src/main/java/com/sun/mail/handlers/multipart_mixed.java b/app/src/main/java/com/sun/mail/handlers/multipart_mixed.java new file mode 100644 index 0000000000..98c7f28357 --- /dev/null +++ b/app/src/main/java/com/sun/mail/handlers/multipart_mixed.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import java.io.*; +import javax.activation.*; +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.internet.MimeMultipart; + + +public class multipart_mixed extends handler_base { + private static ActivationDataFlavor[] myDF = { + new ActivationDataFlavor(Multipart.class, + "multipart/mixed", "Multipart") + }; + + @Override + protected ActivationDataFlavor[] getDataFlavors() { + return myDF; + } + + /** + * Return the content. + */ + @Override + public Object getContent(DataSource ds) throws IOException { + try { + return new MimeMultipart(ds); + } catch (MessagingException e) { + IOException ioex = + new IOException("Exception while constructing MimeMultipart"); + ioex.initCause(e); + throw ioex; + } + } + + /** + * Write the object to the output stream, using the specific MIME type. + */ + @Override + public void writeTo(Object obj, String mimeType, OutputStream os) + throws IOException { + if (!(obj instanceof Multipart)) + throw new IOException("\"" + getDataFlavors()[0].getMimeType() + + "\" DataContentHandler requires Multipart object, " + + "was given object of type " + obj.getClass().toString() + + "; obj.cl " + obj.getClass().getClassLoader() + + ", Multipart.cl " + Multipart.class.getClassLoader()); + + try { + ((Multipart)obj).writeTo(os); + } catch (MessagingException e) { + IOException ioex = + new IOException("Exception writing Multipart"); + ioex.initCause(e); + throw ioex; + } + } +} diff --git a/app/src/main/java/com/sun/mail/handlers/package.html b/app/src/main/java/com/sun/mail/handlers/package.html new file mode 100644 index 0000000000..0aafa21aba --- /dev/null +++ b/app/src/main/java/com/sun/mail/handlers/package.html @@ -0,0 +1,33 @@ + + + + + + +com.sun.mail.handlers package + + + +

+This package includes internal data handler support classes and +SHOULD NOT BE USED DIRECTLY BY APPLICATIONS. +

+ + + diff --git a/app/src/main/java/com/sun/mail/handlers/text_html.java b/app/src/main/java/com/sun/mail/handlers/text_html.java new file mode 100644 index 0000000000..24ef058520 --- /dev/null +++ b/app/src/main/java/com/sun/mail/handlers/text_html.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import javax.activation.ActivationDataFlavor; + +/** + * DataContentHandler for text/html. + * + */ +public class text_html extends text_plain { + private static ActivationDataFlavor[] myDF = { + new ActivationDataFlavor(String.class, "text/html", "HTML String") + }; + + @Override + protected ActivationDataFlavor[] getDataFlavors() { + return myDF; + } +} diff --git a/app/src/main/java/com/sun/mail/handlers/text_plain.java b/app/src/main/java/com/sun/mail/handlers/text_plain.java new file mode 100644 index 0000000000..da0beabd02 --- /dev/null +++ b/app/src/main/java/com/sun/mail/handlers/text_plain.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import java.io.*; +import javax.activation.*; +import javax.mail.internet.ContentType; +import javax.mail.internet.MimeUtility; + +/** + * DataContentHandler for text/plain. + * + */ +public class text_plain extends handler_base { + private static ActivationDataFlavor[] myDF = { + new ActivationDataFlavor(String.class, "text/plain", "Text String") + }; + + /** + * An OuputStream wrapper that doesn't close the underlying stream. + */ + private static class NoCloseOutputStream extends FilterOutputStream { + public NoCloseOutputStream(OutputStream os) { + super(os); + } + + @Override + public void close() { + // do nothing + } + } + + @Override + protected ActivationDataFlavor[] getDataFlavors() { + return myDF; + } + + @Override + public Object getContent(DataSource ds) throws IOException { + String enc = null; + InputStreamReader is = null; + + try { + enc = getCharset(ds.getContentType()); + is = new InputStreamReader(ds.getInputStream(), enc); + } catch (IllegalArgumentException iex) { + /* + * An unknown charset of the form ISO-XXX-XXX will cause + * the JDK to throw an IllegalArgumentException. The + * JDK will attempt to create a classname using this string, + * but valid classnames must not contain the character '-', + * and this results in an IllegalArgumentException, rather than + * the expected UnsupportedEncodingException. Yikes. + */ + throw new UnsupportedEncodingException(enc); + } + + try { + int pos = 0; + int count; + char buf[] = new char[1024]; + + while ((count = is.read(buf, pos, buf.length - pos)) != -1) { + pos += count; + if (pos >= buf.length) { + int size = buf.length; + if (size < 256*1024) + size += size; + else + size += 256*1024; + char tbuf[] = new char[size]; + System.arraycopy(buf, 0, tbuf, 0, pos); + buf = tbuf; + } + } + return new String(buf, 0, pos); + } finally { + try { + is.close(); + } catch (IOException ex) { + // ignore it + } + } + } + + /** + * Write the object to the output stream, using the specified MIME type. + */ + @Override + public void writeTo(Object obj, String type, OutputStream os) + throws IOException { + if (!(obj instanceof String)) + throw new IOException("\"" + getDataFlavors()[0].getMimeType() + + "\" DataContentHandler requires String object, " + + "was given object of type " + obj.getClass().toString()); + + String enc = null; + OutputStreamWriter osw = null; + + try { + enc = getCharset(type); + osw = new OutputStreamWriter(new NoCloseOutputStream(os), enc); + } catch (IllegalArgumentException iex) { + /* + * An unknown charset of the form ISO-XXX-XXX will cause + * the JDK to throw an IllegalArgumentException. The + * JDK will attempt to create a classname using this string, + * but valid classnames must not contain the character '-', + * and this results in an IllegalArgumentException, rather than + * the expected UnsupportedEncodingException. Yikes. + */ + throw new UnsupportedEncodingException(enc); + } + + String s = (String)obj; + osw.write(s, 0, s.length()); + /* + * Have to call osw.close() instead of osw.flush() because + * some charset converts, such as the iso-2022-jp converter, + * don't output the "shift out" sequence unless they're closed. + * The NoCloseOutputStream wrapper prevents the underlying + * stream from being closed. + */ + osw.close(); + } + + private String getCharset(String type) { + try { + ContentType ct = new ContentType(type); + String charset = ct.getParameter("charset"); + if (charset == null) + // If the charset parameter is absent, use US-ASCII. + charset = "us-ascii"; + return MimeUtility.javaCharset(charset); + } catch (Exception ex) { + return null; + } + } +} diff --git a/app/src/main/java/com/sun/mail/handlers/text_xml.java b/app/src/main/java/com/sun/mail/handlers/text_xml.java new file mode 100644 index 0000000000..1f0301793c --- /dev/null +++ b/app/src/main/java/com/sun/mail/handlers/text_xml.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import java.io.IOException; +import java.io.OutputStream; + +import javax.activation.ActivationDataFlavor; +import javax.activation.DataSource; +import javax.mail.internet.ContentType; +import javax.mail.internet.ParseException; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.TransformerException; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +/** + * DataContentHandler for text/xml. + * + * @author Anil Vijendran + * @author Bill Shannon + */ +public class text_xml extends text_plain { + + private static final ActivationDataFlavor[] flavors = { + new ActivationDataFlavor(String.class, "text/xml", "XML String"), + new ActivationDataFlavor(String.class, "application/xml", "XML String"), + new ActivationDataFlavor(StreamSource.class, "text/xml", "XML"), + new ActivationDataFlavor(StreamSource.class, "application/xml", "XML") + }; + + @Override + protected ActivationDataFlavor[] getDataFlavors() { + return flavors; + } + + @Override + protected Object getData(ActivationDataFlavor aFlavor, DataSource ds) + throws IOException { + if (aFlavor.getRepresentationClass() == String.class) + return super.getContent(ds); + else if (aFlavor.getRepresentationClass() == StreamSource.class) + return new StreamSource(ds.getInputStream()); + else + return null; // XXX - should never happen + } + + /** + */ + @Override + public void writeTo(Object obj, String mimeType, OutputStream os) + throws IOException { + if (!isXmlType(mimeType)) + throw new IOException( + "Invalid content type \"" + mimeType + "\" for text/xml DCH"); + if (obj instanceof String) { + super.writeTo(obj, mimeType, os); + return; + } + if (!(obj instanceof DataSource || obj instanceof Source)) { + throw new IOException("Invalid Object type = "+obj.getClass()+ + ". XmlDCH can only convert DataSource or Source to XML."); + } + + try { + Transformer transformer = + TransformerFactory.newInstance().newTransformer(); + StreamResult result = new StreamResult(os); + if (obj instanceof DataSource) { + // Streaming transform applies only to + // javax.xml.transform.StreamSource + transformer.transform( + new StreamSource(((DataSource)obj).getInputStream()), + result); + } else { + transformer.transform((Source)obj, result); + } + } catch (TransformerException ex) { + IOException ioex = new IOException( + "Unable to run the JAXP transformer on a stream " + + ex.getMessage()); + ioex.initCause(ex); + throw ioex; + } catch (RuntimeException ex) { + IOException ioex = new IOException( + "Unable to run the JAXP transformer on a stream " + + ex.getMessage()); + ioex.initCause(ex); + throw ioex; + } + } + + private boolean isXmlType(String type) { + try { + ContentType ct = new ContentType(type); + return ct.getSubType().equals("xml") && + (ct.getPrimaryType().equals("text") || + ct.getPrimaryType().equals("application")); + } catch (ParseException ex) { + return false; + } catch (RuntimeException ex) { + return false; + } + } +} diff --git a/app/src/main/java/com/sun/mail/iap/Argument.java b/app/src/main/java/com/sun/mail/iap/Argument.java new file mode 100644 index 0000000000..677c63dc52 --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/Argument.java @@ -0,0 +1,431 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +import java.util.List; +import java.util.ArrayList; +import java.io.*; +import java.nio.charset.Charset; + +import com.sun.mail.util.ASCIIUtility; + +/** + * @author John Mani + * @author Bill Shannon + */ + +public class Argument { + protected List items; + + /** + * Constructor + */ + public Argument() { + items = new ArrayList<>(1); + } + + /** + * Append the given Argument to this Argument. All items + * from the source argument are copied into this destination + * argument. + * + * @param arg the Argument to append + * @return this + */ + public Argument append(Argument arg) { + items.addAll(arg.items); + return this; + } + + /** + * Write out given string as an ASTRING, depending on the type + * of the characters inside the string. The string should + * contain only ASCII characters.

+ * + * XXX: Hmm .. this should really be called writeASCII() + * + * @param s String to write out + * @return this + */ + public Argument writeString(String s) { + items.add(new AString(ASCIIUtility.getBytes(s))); + return this; + } + + /** + * Convert the given string into bytes in the specified + * charset, and write the bytes out as an ASTRING + * + * @param s String to write out + * @param charset the charset + * @return this + * @exception UnsupportedEncodingException for bad charset + */ + public Argument writeString(String s, String charset) + throws UnsupportedEncodingException { + if (charset == null) // convenience + writeString(s); + else + items.add(new AString(s.getBytes(charset))); + return this; + } + + /** + * Convert the given string into bytes in the specified + * charset, and write the bytes out as an ASTRING + * + * @param s String to write out + * @param charset the charset + * @return this + * @since JavaMail 1.6.0 + */ + public Argument writeString(String s, Charset charset) { + if (charset == null) // convenience + writeString(s); + else + items.add(new AString(s.getBytes(charset))); + return this; + } + + /** + * Write out given string as an NSTRING, depending on the type + * of the characters inside the string. The string should + * contain only ASCII characters.

+ * + * @param s String to write out + * @return this + * @since JavaMail 1.5.1 + */ + public Argument writeNString(String s) { + if (s == null) + items.add(new NString(null)); + else + items.add(new NString(ASCIIUtility.getBytes(s))); + return this; + } + + /** + * Convert the given string into bytes in the specified + * charset, and write the bytes out as an NSTRING + * + * @param s String to write out + * @param charset the charset + * @return this + * @exception UnsupportedEncodingException for bad charset + * @since JavaMail 1.5.1 + */ + public Argument writeNString(String s, String charset) + throws UnsupportedEncodingException { + if (s == null) + items.add(new NString(null)); + else if (charset == null) // convenience + writeString(s); + else + items.add(new NString(s.getBytes(charset))); + return this; + } + + /** + * Convert the given string into bytes in the specified + * charset, and write the bytes out as an NSTRING + * + * @param s String to write out + * @param charset the charset + * @return this + * @since JavaMail 1.6.0 + */ + public Argument writeNString(String s, Charset charset) { + if (s == null) + items.add(new NString(null)); + else if (charset == null) // convenience + writeString(s); + else + items.add(new NString(s.getBytes(charset))); + return this; + } + + /** + * Write out given byte[] as a Literal. + * @param b byte[] to write out + * @return this + */ + public Argument writeBytes(byte[] b) { + items.add(b); + return this; + } + + /** + * Write out given ByteArrayOutputStream as a Literal. + * @param b ByteArrayOutputStream to be written out. + * @return this + */ + public Argument writeBytes(ByteArrayOutputStream b) { + items.add(b); + return this; + } + + /** + * Write out given data as a literal. + * @param b Literal representing data to be written out. + * @return this + */ + public Argument writeBytes(Literal b) { + items.add(b); + return this; + } + + /** + * Write out given string as an Atom. Note that an Atom can contain only + * certain US-ASCII characters. No validation is done on the characters + * in the string. + * @param s String + * @return this + */ + public Argument writeAtom(String s) { + items.add(new Atom(s)); + return this; + } + + /** + * Write out number. + * @param i number + * @return this + */ + public Argument writeNumber(int i) { + items.add(Integer.valueOf(i)); + return this; + } + + /** + * Write out number. + * @param i number + * @return this + */ + public Argument writeNumber(long i) { + items.add(Long.valueOf(i)); + return this; + } + + /** + * Write out as parenthesised list. + * + * @param c the Argument + * @return this + */ + public Argument writeArgument(Argument c) { + items.add(c); + return this; + } + + /* + * Write out all the buffered items into the output stream. + */ + public void write(Protocol protocol) + throws IOException, ProtocolException { + int size = items != null ? items.size() : 0; + DataOutputStream os = (DataOutputStream)protocol.getOutputStream(); + + for (int i=0; i < size; i++) { + if (i > 0) // write delimiter if not the first item + os.write(' '); + + Object o = items.get(i); + if (o instanceof Atom) { + os.writeBytes(((Atom)o).string); + } else if (o instanceof Number) { + os.writeBytes(((Number)o).toString()); + } else if (o instanceof AString) { + astring(((AString)o).bytes, protocol); + } else if (o instanceof NString) { + nstring(((NString)o).bytes, protocol); + } else if (o instanceof byte[]) { + literal((byte[])o, protocol); + } else if (o instanceof ByteArrayOutputStream) { + literal((ByteArrayOutputStream)o, protocol); + } else if (o instanceof Literal) { + literal((Literal)o, protocol); + } else if (o instanceof Argument) { + os.write('('); // open parans + ((Argument)o).write(protocol); + os.write(')'); // close parans + } + } + } + + /** + * Write out given String as either an Atom, QuotedString or Literal + */ + private void astring(byte[] bytes, Protocol protocol) + throws IOException, ProtocolException { + nastring(bytes, protocol, false); + } + + /** + * Write out given String as either NIL, QuotedString, or Literal. + */ + private void nstring(byte[] bytes, Protocol protocol) + throws IOException, ProtocolException { + if (bytes == null) { + DataOutputStream os = (DataOutputStream)protocol.getOutputStream(); + os.writeBytes("NIL"); + } else + nastring(bytes, protocol, true); + } + + private void nastring(byte[] bytes, Protocol protocol, boolean doQuote) + throws IOException, ProtocolException { + DataOutputStream os = (DataOutputStream)protocol.getOutputStream(); + int len = bytes.length; + + // If length is greater than 1024 bytes, send as literal + if (len > 1024) { + literal(bytes, protocol); + return; + } + + // if 0 length, send as quoted-string + boolean quote = len == 0 ? true : doQuote; + boolean escape = false; + boolean utf8 = protocol.supportsUtf8(); + + byte b; + for (int i = 0; i < len; i++) { + b = bytes[i]; + if (b == '\0' || b == '\r' || b == '\n' || + (!utf8 && ((b & 0xff) > 0177))) { + // NUL, CR or LF means the bytes need to be sent as literals + literal(bytes, protocol); + return; + } + if (b == '*' || b == '%' || b == '(' || b == ')' || b == '{' || + b == '"' || b == '\\' || + ((b & 0xff) <= ' ') || ((b & 0xff) > 0177)) { + quote = true; + if (b == '"' || b == '\\') // need to escape these characters + escape = true; + } + } + + /* + * Make sure the (case-independent) string "NIL" is always quoted, + * so as not to be confused with a real NIL (handled above in nstring). + * This is more than is necessary, but it's rare to begin with and + * this makes it safer than doing the test in nstring above in case + * some code calls writeString when it should call writeNString. + */ + if (!quote && bytes.length == 3 && + (bytes[0] == 'N' || bytes[0] == 'n') && + (bytes[1] == 'I' || bytes[1] == 'i') && + (bytes[2] == 'L' || bytes[2] == 'l')) + quote = true; + + if (quote) // start quote + os.write('"'); + + if (escape) { + // already quoted + for (int i = 0; i < len; i++) { + b = bytes[i]; + if (b == '"' || b == '\\') + os.write('\\'); + os.write(b); + } + } else + os.write(bytes); + + + if (quote) // end quote + os.write('"'); + } + + /** + * Write out given byte[] as a literal + */ + private void literal(byte[] b, Protocol protocol) + throws IOException, ProtocolException { + startLiteral(protocol, b.length).write(b); + } + + /** + * Write out given ByteArrayOutputStream as a literal. + */ + private void literal(ByteArrayOutputStream b, Protocol protocol) + throws IOException, ProtocolException { + b.writeTo(startLiteral(protocol, b.size())); + } + + /** + * Write out given Literal as a literal. + */ + private void literal(Literal b, Protocol protocol) + throws IOException, ProtocolException { + b.writeTo(startLiteral(protocol, b.size())); + } + + private OutputStream startLiteral(Protocol protocol, int size) + throws IOException, ProtocolException { + DataOutputStream os = (DataOutputStream)protocol.getOutputStream(); + boolean nonSync = protocol.supportsNonSyncLiterals(); + + os.write('{'); + os.writeBytes(Integer.toString(size)); + if (nonSync) // server supports non-sync literals + os.writeBytes("+}\r\n"); + else + os.writeBytes("}\r\n"); + os.flush(); + + // If we are using synchronized literals, wait for the server's + // continuation signal + if (!nonSync) { + for (; ;) { + Response r = protocol.readResponse(); + if (r.isContinuation()) + break; + if (r.isTagged()) + throw new LiteralException(r); + // XXX - throw away untagged responses; + // violates IMAP spec, hope no servers do this + } + } + return os; + } +} + +class Atom { + String string; + + Atom(String s) { + string = s; + } +} + +class AString { + byte[] bytes; + + AString(byte[] b) { + bytes = b; + } +} + +class NString { + byte[] bytes; + + NString(byte[] b) { + bytes = b; + } +} diff --git a/app/src/main/java/com/sun/mail/iap/BadCommandException.java b/app/src/main/java/com/sun/mail/iap/BadCommandException.java new file mode 100644 index 0000000000..4a1b33bf6e --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/BadCommandException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +/** + * @author John Mani + */ + +public class BadCommandException extends ProtocolException { + + private static final long serialVersionUID = 5769722539397237515L; + + /** + * Constructs an BadCommandException with no detail message. + */ + public BadCommandException() { + super(); + } + + /** + * Constructs an BadCommandException with the specified detail message. + * @param s the detail message + */ + public BadCommandException(String s) { + super(s); + } + + /** + * Constructs an BadCommandException with the specified Response. + * @param r the Response + */ + public BadCommandException(Response r) { + super(r); + } +} diff --git a/app/src/main/java/com/sun/mail/iap/ByteArray.java b/app/src/main/java/com/sun/mail/iap/ByteArray.java new file mode 100644 index 0000000000..e05110bcaf --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/ByteArray.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +import java.io.ByteArrayInputStream; + +/** + * A simple wrapper around a byte array, with a start position and + * count of bytes. + * + * @author John Mani + */ + +public class ByteArray { + private byte[] bytes; // the byte array + private int start; // start position + private int count; // count of bytes + + /** + * Constructor + * + * @param b the byte array to wrap + * @param start start position in byte array + * @param count number of bytes in byte array + */ + public ByteArray(byte[] b, int start, int count) { + bytes = b; + this.start = start; + this.count = count; + } + + /** + * Constructor that creates a byte array of the specified size. + * + * @param size the size of the ByteArray + * @since JavaMail 1.4.1 + */ + public ByteArray(int size) { + this(new byte[size], 0, size); + } + + /** + * Returns the internal byte array. Note that this is a live + * reference to the actual data, not a copy. + * + * @return the wrapped byte array + */ + public byte[] getBytes() { + return bytes; + } + + /** + * Returns a new byte array that is a copy of the data. + * + * @return a new byte array with the bytes from start for count + */ + public byte[] getNewBytes() { + byte[] b = new byte[count]; + System.arraycopy(bytes, start, b, 0, count); + return b; + } + + /** + * Returns the start position + * + * @return the start position + */ + public int getStart() { + return start; + } + + /** + * Returns the count of bytes + * + * @return the number of bytes + */ + public int getCount() { + return count; + } + + /** + * Set the count of bytes. + * + * @param count the number of bytes + * @since JavaMail 1.4.1 + */ + public void setCount(int count) { + this.count = count; + } + + /** + * Returns a ByteArrayInputStream. + * + * @return the ByteArrayInputStream + */ + public ByteArrayInputStream toByteArrayInputStream() { + return new ByteArrayInputStream(bytes, start, count); + } + + /** + * Grow the byte array by incr bytes. + * + * @param incr how much to grow + * @since JavaMail 1.4.1 + */ + public void grow(int incr) { + byte[] nbuf = new byte[bytes.length + incr]; + System.arraycopy(bytes, 0, nbuf, 0, bytes.length); + bytes = nbuf; + } +} diff --git a/app/src/main/java/com/sun/mail/iap/CommandFailedException.java b/app/src/main/java/com/sun/mail/iap/CommandFailedException.java new file mode 100644 index 0000000000..8c141e7ba5 --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/CommandFailedException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +/** + * @author John Mani + */ + +public class CommandFailedException extends ProtocolException { + + private static final long serialVersionUID = 793932807880443631L; + + /** + * Constructs an CommandFailedException with no detail message. + */ + public CommandFailedException() { + super(); + } + + /** + * Constructs an CommandFailedException with the specified detail message. + * @param s the detail message + */ + public CommandFailedException(String s) { + super(s); + } + + /** + * Constructs an CommandFailedException with the specified Response. + * @param r the Response. + */ + public CommandFailedException(Response r) { + super(r); + } +} diff --git a/app/src/main/java/com/sun/mail/iap/ConnectionException.java b/app/src/main/java/com/sun/mail/iap/ConnectionException.java new file mode 100644 index 0000000000..12dd496106 --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/ConnectionException.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +/** + * @author John Mani + */ + +public class ConnectionException extends ProtocolException { + private transient Protocol p; + + private static final long serialVersionUID = 5749739604257464727L; + + /** + * Constructs an ConnectionException with no detail message. + */ + public ConnectionException() { + super(); + } + + /** + * Constructs an ConnectionException with the specified detail message. + * + * @param s the detail message + */ + public ConnectionException(String s) { + super(s); + } + + /** + * Constructs an ConnectionException with the specified Response. + * + * @param p the Protocol object + * @param r the Response + */ + public ConnectionException(Protocol p, Response r) { + super(r); + this.p = p; + } + + public Protocol getProtocol() { + return p; + } +} diff --git a/app/src/main/java/com/sun/mail/iap/Literal.java b/app/src/main/java/com/sun/mail/iap/Literal.java new file mode 100644 index 0000000000..14c592b274 --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/Literal.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +import java.io.*; + +/** + * An interface for objects that provide data dynamically for use in + * a literal protocol element. + * + * @author Bill Shannon + */ + +public interface Literal { + /** + * Return the size of the data. + * + * @return the size of the data + */ + public int size(); + + /** + * Write the data to the OutputStream. + * + * @param os the output stream + * @exception IOException for I/O errors + */ + public void writeTo(OutputStream os) throws IOException; +} diff --git a/app/src/main/java/com/sun/mail/iap/LiteralException.java b/app/src/main/java/com/sun/mail/iap/LiteralException.java new file mode 100644 index 0000000000..660747f442 --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/LiteralException.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +/** + * @author Bill Shannon + */ + +public class LiteralException extends ProtocolException { + + private static final long serialVersionUID = -6919179828339609913L; + + /** + * Constructs a LiteralException with the specified Response object. + * + * @param r the response object + */ + public LiteralException(Response r) { + super(r.toString()); + response = r; + } +} diff --git a/app/src/main/java/com/sun/mail/iap/ParsingException.java b/app/src/main/java/com/sun/mail/iap/ParsingException.java new file mode 100644 index 0000000000..a1e7168a8f --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/ParsingException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +/** + * @author John Mani + */ + +public class ParsingException extends ProtocolException { + + private static final long serialVersionUID = 7756119840142724839L; + + /** + * Constructs an ParsingException with no detail message. + */ + public ParsingException() { + super(); + } + + /** + * Constructs an ParsingException with the specified detail message. + * @param s the detail message + */ + public ParsingException(String s) { + super(s); + } + + /** + * Constructs an ParsingException with the specified Response. + * @param r the Response + */ + public ParsingException(Response r) { + super(r); + } +} diff --git a/app/src/main/java/com/sun/mail/iap/Protocol.java b/app/src/main/java/com/sun/mail/iap/Protocol.java new file mode 100644 index 0000000000..b7bd688248 --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/Protocol.java @@ -0,0 +1,682 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +import java.util.Properties; +import java.io.*; +import java.nio.channels.SocketChannel; +import java.net.*; +import javax.net.ssl.SSLSocket; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; + +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.SocketFetcher; +import com.sun.mail.util.TraceInputStream; +import com.sun.mail.util.TraceOutputStream; + +/** + * General protocol handling code for IMAP-like protocols.

+ * + * The Protocol object is multithread safe. + * + * @author John Mani + * @author Max Spivak + * @author Bill Shannon + */ + +public class Protocol { + protected String host; + private Socket socket; + // in case we turn on TLS, we'll need these later + protected boolean quote; + protected MailLogger logger; + protected MailLogger traceLogger; + protected Properties props; + protected String prefix; + + private TraceInputStream traceInput; // the Tracer + private volatile ResponseInputStream input; + + private TraceOutputStream traceOutput; // the Tracer + private volatile DataOutputStream output; + + private int tagCounter = 0; + private final String tagPrefix; + + private String localHostName; + + private final List handlers + = new CopyOnWriteArrayList<>(); + + private volatile long timestamp; + + // package private, to allow testing + static final AtomicInteger tagNum = new AtomicInteger(); + + private static final byte[] CRLF = { (byte)'\r', (byte)'\n'}; + + /** + * Constructor.

+ * + * Opens a connection to the given host at given port. + * + * @param host host to connect to + * @param port portnumber to connect to + * @param props Properties object used by this protocol + * @param prefix Prefix to prepend to property keys + * @param isSSL use SSL? + * @param logger log messages here + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + public Protocol(String host, int port, + Properties props, String prefix, + boolean isSSL, MailLogger logger) + throws IOException, ProtocolException { + boolean connected = false; // did constructor succeed? + tagPrefix = computePrefix(props, prefix); + try { + this.host = host; + this.props = props; + this.prefix = prefix; + this.logger = logger; + traceLogger = logger.getSubLogger("protocol", null); + + socket = SocketFetcher.getSocket(host, port, props, prefix, isSSL); + quote = PropUtil.getBooleanProperty(props, + "mail.debug.quote", false); + + initStreams(); + + // Read server greeting + processGreeting(readResponse()); + + timestamp = System.currentTimeMillis(); + + connected = true; // must be last statement in constructor + } finally { + /* + * If we get here because an exception was thrown, we need + * to disconnect to avoid leaving a connected socket that + * no one will be able to use because this object was never + * completely constructed. + */ + if (!connected) + disconnect(); + } + } + + private void initStreams() throws IOException { + traceInput = new TraceInputStream(socket.getInputStream(), traceLogger); + traceInput.setQuote(quote); + input = new ResponseInputStream(traceInput); + + traceOutput = + new TraceOutputStream(socket.getOutputStream(), traceLogger); + traceOutput.setQuote(quote); + output = new DataOutputStream(new BufferedOutputStream(traceOutput)); + } + + /** + * Compute the tag prefix to be used for this connection. + * Start with "A" - "Z", then "AA" - "ZZ", and finally "AAA" - "ZZZ". + * Wrap around after that. + */ + private String computePrefix(Properties props, String prefix) { + // XXX - in case someone depends on the tag prefix + if (PropUtil.getBooleanProperty(props, + prefix + ".reusetagprefix", false)) + return "A"; + // tag prefix, wrap around after three letters + int n = tagNum.getAndIncrement() % (26*26*26 + 26*26 + 26); + String tagPrefix; + if (n < 26) + tagPrefix = new String(new char[] { (char)('A' + n) }); + else if (n < (26*26 + 26)) { + n -= 26; + tagPrefix = new String(new char[] { + (char)('A' + n/26), (char)('A' + n%26) }); + } else { + n -= (26*26 + 26); + tagPrefix = new String(new char[] { + (char)('A' + n/(26*26)), + (char)('A' + (n%(26*26))/26), + (char)('A' + n%26) }); + } + return tagPrefix; + } + + /** + * Constructor for debugging. + * + * @param in the InputStream to read from + * @param out the PrintStream to write to + * @param props Properties object used by this protocol + * @param debug true to enable debugging output + * @exception IOException for I/O errors + */ + public Protocol(InputStream in, PrintStream out, Properties props, + boolean debug) throws IOException { + this.host = "localhost"; + this.props = props; + this.quote = false; + tagPrefix = computePrefix(props, "mail.imap"); + logger = new MailLogger(this.getClass(), "DEBUG", debug, System.out); + traceLogger = logger.getSubLogger("protocol", null); + + // XXX - inlined initStreams, won't allow later startTLS + traceInput = new TraceInputStream(in, traceLogger); + traceInput.setQuote(quote); + input = new ResponseInputStream(traceInput); + + traceOutput = new TraceOutputStream(out, traceLogger); + traceOutput.setQuote(quote); + output = new DataOutputStream(new BufferedOutputStream(traceOutput)); + + timestamp = System.currentTimeMillis(); + } + + /** + * Returns the timestamp. + * + * @return the timestamp + */ + public long getTimestamp() { + return timestamp; + } + + /** + * Adds a response handler. + * + * @param h the response handler + */ + public void addResponseHandler(ResponseHandler h) { + handlers.add(h); + } + + /** + * Removed the specified response handler. + * + * @param h the response handler + */ + public void removeResponseHandler(ResponseHandler h) { + handlers.remove(h); + } + + /** + * Notify response handlers + * + * @param responses the responses + */ + public void notifyResponseHandlers(Response[] responses) { + if (handlers.isEmpty()) { + return; + } + + for (Response r : responses) { + if (r != null) { + for (ResponseHandler rh : handlers) { + if (rh != null) { + rh.handleResponse(r); + } + } + } + } + } + + protected void processGreeting(Response r) throws ProtocolException { + if (r.isBYE()) + throw new ConnectionException(this, r); + } + + /** + * Return the Protocol's InputStream. + * + * @return the input stream + */ + protected ResponseInputStream getInputStream() { + return input; + } + + /** + * Return the Protocol's OutputStream + * + * @return the output stream + */ + protected OutputStream getOutputStream() { + return output; + } + + /** + * Returns whether this Protocol supports non-synchronizing literals + * Default is false. Subclasses should override this if required + * + * @return true if the server supports non-synchronizing literals + */ + protected synchronized boolean supportsNonSyncLiterals() { + return false; + } + + public Response readResponse() + throws IOException, ProtocolException { + return new Response(this); + } + + /** + * Is another response available in our buffer? + * + * @return true if another response is in the buffer + * @since JavaMail 1.5.4 + */ + public boolean hasResponse() { + /* + * XXX - Really should peek ahead in the buffer to see + * if there's a *complete* response available, but if there + * isn't who's going to read more data into the buffer + * until there is? + */ + try { + return input.available() > 0; + } catch (IOException ex) { + } + return false; + } + + /** + * Return a buffer to be used to read a response. + * The default implementation returns null, which causes + * a new buffer to be allocated for every response. + * + * @return the buffer to use + * @since JavaMail 1.4.1 + */ + protected ByteArray getResponseBuffer() { + return null; + } + + public String writeCommand(String command, Argument args) + throws IOException, ProtocolException { + // assert Thread.holdsLock(this); + // can't assert because it's called from constructor + String tag = tagPrefix + Integer.toString(tagCounter++); // unique tag + + output.writeBytes(tag + " " + command); + + if (args != null) { + output.write(' '); + args.write(this); + } + + output.write(CRLF); + output.flush(); + return tag; + } + + /** + * Send a command to the server. Collect all responses until either + * the corresponding command completion response or a BYE response + * (indicating server failure). Return all the collected responses. + * + * @param command the command + * @param args the arguments + * @return array of Response objects returned by the server + */ + public synchronized Response[] command(String command, Argument args) { + commandStart(command); + List v = new ArrayList<>(); + boolean done = false; + String tag = null; + + // write the command + try { + tag = writeCommand(command, args); + } catch (LiteralException lex) { + v.add(lex.getResponse()); + done = true; + } catch (Exception ex) { + // Convert this into a BYE response + v.add(Response.byeResponse(ex)); + done = true; + } + + Response byeResp = null; + while (!done) { + Response r = null; + try { + r = readResponse(); + } catch (IOException ioex) { + if (byeResp == null) // convert this into a BYE response + byeResp = Response.byeResponse(ioex); + // else, connection closed after BYE was sent + break; + } catch (ProtocolException pex) { + logger.log(Level.FINE, "ignoring bad response", pex); + continue; // skip this response + } + + if (r.isBYE()) { + byeResp = r; + continue; + } + + v.add(r); + + // If this is a matching command completion response, we are done + if (r.isTagged() && r.getTag().equals(tag)) + done = true; + } + + if (byeResp != null) + v.add(byeResp); // must be last + Response[] responses = new Response[v.size()]; + v.toArray(responses); + timestamp = System.currentTimeMillis(); + commandEnd(); + return responses; + } + + /** + * Convenience routine to handle OK, NO, BAD and BYE responses. + * + * @param response the response + * @exception ProtocolException for protocol failures + */ + public void handleResult(Response response) throws ProtocolException { + if (response.isOK()) + return; + else if (response.isNO()) + throw new CommandFailedException(response); + else if (response.isBAD()) + throw new BadCommandException(response); + else if (response.isBYE()) { + disconnect(); + throw new ConnectionException(this, response); + } + } + + /** + * Convenience routine to handle simple IAP commands + * that do not have responses specific to that command. + * + * @param cmd the command + * @param args the arguments + * @exception ProtocolException for protocol failures + */ + public void simpleCommand(String cmd, Argument args) + throws ProtocolException { + // Issue command + Response[] r = command(cmd, args); + + // dispatch untagged responses + notifyResponseHandlers(r); + + // Handle result of this command + handleResult(r[r.length-1]); + } + + /** + * Start TLS on the current connection. + * cmd is the command to issue to start TLS negotiation. + * If the command succeeds, we begin TLS negotiation. + * If the socket is already an SSLSocket this is a nop and the command + * is not issued. + * + * @param cmd the command to issue + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + public synchronized void startTLS(String cmd) + throws IOException, ProtocolException { + if (socket instanceof SSLSocket) + return; // nothing to do + simpleCommand(cmd, null); + socket = SocketFetcher.startTLS(socket, host, props, prefix); + initStreams(); + } + + /** + * Start compression on the current connection. + * cmd is the command to issue to start compression. + * If the command succeeds, we begin compression. + * + * @param cmd the command to issue + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + public synchronized void startCompression(String cmd) + throws IOException, ProtocolException { + // XXX - check whether compression is already enabled? + simpleCommand(cmd, null); + + // need to create our own Inflater and Deflater in order to set nowrap + Inflater inf = new Inflater(true); + traceInput = new TraceInputStream(new InflaterInputStream( + socket.getInputStream(), inf), traceLogger); + traceInput.setQuote(quote); + input = new ResponseInputStream(traceInput); + + // configure the Deflater + int level = PropUtil.getIntProperty(props, prefix + ".compress.level", + Deflater.DEFAULT_COMPRESSION); + int strategy = PropUtil.getIntProperty(props, + prefix + ".compress.strategy", + Deflater.DEFAULT_STRATEGY); + if (logger.isLoggable(Level.FINE)) + logger.log(Level.FINE, + "Creating Deflater with compression level {0} and strategy {1}", + new Object[] { level, strategy }); + Deflater def = new Deflater(Deflater.DEFAULT_COMPRESSION, true); + try { + def.setLevel(level); + } catch (IllegalArgumentException ex) { + logger.log(Level.FINE, "Ignoring bad compression level", ex); + } + try { + def.setStrategy(strategy); + } catch (IllegalArgumentException ex) { + logger.log(Level.FINE, "Ignoring bad compression strategy", ex); + } + traceOutput = new TraceOutputStream(new DeflaterOutputStream( + socket.getOutputStream(), def, true), traceLogger); + traceOutput.setQuote(quote); + output = new DataOutputStream(new BufferedOutputStream(traceOutput)); + } + + /** + * Is this connection using an SSL socket? + * + * @return true if using SSL + * @since JavaMail 1.4.6 + */ + public boolean isSSL() { + return socket instanceof SSLSocket; + } + + /** + * Return the address the socket connected to. + * + * @return the InetAddress the socket is connected to + * @since JavaMail 1.5.2 + */ + public InetAddress getInetAddress() { + return socket.getInetAddress(); + } + + /** + * Return the SocketChannel associated with this connection, if any. + * + * @return the SocketChannel + * @since JavaMail 1.5.2 + */ + public SocketChannel getChannel() { + SocketChannel ret = socket.getChannel(); + if (ret != null) + return ret; + + // XXX - Android is broken and SSL wrapped sockets don't delegate + // the getChannel method to the wrapped Socket + if (socket instanceof SSLSocket) { + try { + Field f = socket.getClass().getDeclaredField("socket"); + f.setAccessible(true); + Socket s = (Socket)f.get(socket); + ret = s.getChannel(); + } catch (Exception ex) { + // ignore anything that might go wrong + } + } + return ret; + } + + /** + * Return the local SocketAddress (host and port) for this + * end of the connection. + * + * @return the SocketAddress + * @since Jakarta Mail 1.6.4 + */ + public SocketAddress getLocalSocketAddress() { + return socket.getLocalSocketAddress(); + } + + /** + * Does the server support UTF-8? + * This implementation returns false. + * Subclasses should override as appropriate. + * + * @return true if the server supports UTF-8 + * @since JavaMail 1.6.0 + */ + public boolean supportsUtf8() { + return false; + } + + /** + * Disconnect. + */ + protected synchronized void disconnect() { + if (socket != null) { + try { + socket.close(); + } catch (IOException e) { + // ignore it + } + socket = null; + } + } + + /** + * Get the name of the local host. + * The property <prefix>.localhost overrides + * <prefix>.localaddress, + * which overrides what InetAddress would tell us. + * + * @return the name of the local host + */ + protected synchronized String getLocalHost() { + // get our hostname and cache it for future use + if (localHostName == null || localHostName.length() <= 0) + localHostName = + props.getProperty(prefix + ".localhost"); + if (localHostName == null || localHostName.length() <= 0) + localHostName = + props.getProperty(prefix + ".localaddress"); + try { + if (localHostName == null || localHostName.length() <= 0) { + InetAddress localHost = InetAddress.getLocalHost(); + localHostName = localHost.getCanonicalHostName(); + // if we can't get our name, use local address literal + if (localHostName == null) + // XXX - not correct for IPv6 + localHostName = "[" + localHost.getHostAddress() + "]"; + } + } catch (UnknownHostException uhex) { + } + + // last chance, try to get our address from our socket + if (localHostName == null || localHostName.length() <= 0) { + if (socket != null && socket.isBound()) { + InetAddress localHost = socket.getLocalAddress(); + localHostName = localHost.getCanonicalHostName(); + // if we can't get our name, use local address literal + if (localHostName == null) + // XXX - not correct for IPv6 + localHostName = "[" + localHost.getHostAddress() + "]"; + } + } + return localHostName; + } + + /** + * Is protocol tracing enabled? + * + * @return true if protocol tracing is enabled + */ + protected boolean isTracing() { + return traceLogger.isLoggable(Level.FINEST); + } + + /** + * Temporarily turn off protocol tracing, e.g., to prevent + * tracing the authentication sequence, including the password. + */ + protected void suspendTracing() { + if (traceLogger.isLoggable(Level.FINEST)) { + traceInput.setTrace(false); + traceOutput.setTrace(false); + } + } + + /** + * Resume protocol tracing, if it was enabled to begin with. + */ + protected void resumeTracing() { + if (traceLogger.isLoggable(Level.FINEST)) { + traceInput.setTrace(true); + traceOutput.setTrace(true); + } + } + + /** + * Finalizer. + */ + @Override + protected void finalize() throws Throwable { + try { + disconnect(); + } finally { + super.finalize(); + } + } + + /* + * Probe points for GlassFish monitoring. + */ + private void commandStart(String command) { } + private void commandEnd() { } +} diff --git a/app/src/main/java/com/sun/mail/iap/ProtocolException.java b/app/src/main/java/com/sun/mail/iap/ProtocolException.java new file mode 100644 index 0000000000..f4d9e3a5d0 --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/ProtocolException.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +/** + * @author John Mani + */ + +public class ProtocolException extends Exception { + protected transient Response response = null; + + private static final long serialVersionUID = -4360500807971797439L; + + /** + * Constructs a ProtocolException with no detail message. + */ + public ProtocolException() { + super(); + } + + /** + * Constructs a ProtocolException with the specified detail message. + * + * @param message the detail message + */ + public ProtocolException(String message) { + super(message); + } + + /** + * Constructs a ProtocolException with the specified detail message + * and cause. + * + * @param message the detail message + * @param cause the cause + */ + public ProtocolException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a ProtocolException with the specified Response object. + * + * @param r the Response + */ + public ProtocolException(Response r) { + super(r.toString()); + response = r; + } + + /** + * Return the offending Response object. + * + * @return the Response object + */ + public Response getResponse() { + return response; + } +} diff --git a/app/src/main/java/com/sun/mail/iap/Response.java b/app/src/main/java/com/sun/mail/iap/Response.java new file mode 100644 index 0000000000..fcc4369957 --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/Response.java @@ -0,0 +1,600 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +import java.io.*; +import java.util.*; +import java.nio.charset.StandardCharsets; + +import com.sun.mail.util.ASCIIUtility; + +/** + * This class represents a response obtained from the input stream + * of an IMAP server. + * + * @author John Mani + * @author Bill Shannon + */ + +public class Response { + protected int index; // internal index (updated during the parse) + protected int pindex; // index after parse, for reset + protected int size; // number of valid bytes in our buffer + protected byte[] buffer = null; + protected int type = 0; + protected String tag = null; + /** @since JavaMail 1.5.4 */ + protected Exception ex; + protected boolean utf8; + + private static final int increment = 100; + + // The first and second bits indicate whether this response + // is a Continuation, Tagged or Untagged + public final static int TAG_MASK = 0x03; + public final static int CONTINUATION = 0x01; + public final static int TAGGED = 0x02; + public final static int UNTAGGED = 0x03; + + // The third, fourth and fifth bits indicate whether this response + // is an OK, NO, BAD or BYE response + public final static int TYPE_MASK = 0x1C; + public final static int OK = 0x04; + public final static int NO = 0x08; + public final static int BAD = 0x0C; + public final static int BYE = 0x10; + + // The sixth bit indicates whether a BYE response is synthetic or real + public final static int SYNTHETIC = 0x20; + + /** + * An ATOM is any CHAR delimited by: + * SPACE | CTL | '(' | ')' | '{' | '%' | '*' | '"' | '\' | ']' + * (CTL is handled in readDelimString.) + */ + private static String ATOM_CHAR_DELIM = " (){%*\"\\]"; + + /** + * An ASTRING_CHAR is any CHAR delimited by: + * SPACE | CTL | '(' | ')' | '{' | '%' | '*' | '"' | '\' + * (CTL is handled in readDelimString.) + */ + private static String ASTRING_CHAR_DELIM = " (){%*\"\\"; + + public Response(String s) { + this(s, true); + } + + /** + * Constructor for testing. + * + * @param s the response string + * @param supportsUtf8 allow UTF-8 in response? + * @since JavaMail 1.6.0 + */ + public Response(String s, boolean supportsUtf8) { + if (supportsUtf8) + buffer = s.getBytes(StandardCharsets.UTF_8); + else + buffer = s.getBytes(StandardCharsets.US_ASCII); + size = buffer.length; + utf8 = supportsUtf8; + parse(); + } + + /** + * Read a new Response from the given Protocol + * + * @param p the Protocol object + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + public Response(Protocol p) throws IOException, ProtocolException { + // read one response into 'buffer' + ByteArray ba = p.getResponseBuffer(); + ByteArray response = p.getInputStream().readResponse(ba); + buffer = response.getBytes(); + size = response.getCount() - 2; // Skip the terminating CRLF + utf8 = p.supportsUtf8(); + + parse(); + } + + /** + * Copy constructor. + * + * @param r the Response to copy + */ + public Response(Response r) { + index = r.index; + pindex = r.pindex; + size = r.size; + buffer = r.buffer; + type = r.type; + tag = r.tag; + ex = r.ex; + utf8 = r.utf8; + } + + /** + * Return a Response object that looks like a BYE protocol response. + * Include the details of the exception in the response string. + * + * @param ex the exception + * @return the synthetic Response object + */ + public static Response byeResponse(Exception ex) { + String err = "* BYE Jakarta Mail Exception: " + ex.toString(); + err = err.replace('\r', ' ').replace('\n', ' '); + Response r = new Response(err); + r.type |= SYNTHETIC; + r.ex = ex; + return r; + } + + /** + * Does the server support UTF-8? + * + * @return true if the server supports UTF-8 + * @since JavaMail 1.6.0 + */ + public boolean supportsUtf8() { + return utf8; + } + + private void parse() { + index = 0; // position internal index at start + + if (size == 0) // empty line + return; + if (buffer[index] == '+') { // Continuation statement + type |= CONTINUATION; + index += 1; // Position beyond the '+' + return; // return + } else if (buffer[index] == '*') { // Untagged statement + type |= UNTAGGED; + index += 1; // Position beyond the '*' + } else { // Tagged statement + type |= TAGGED; + tag = readAtom(); // read the TAG, index positioned beyond tag + if (tag == null) + tag = ""; // avoid possible NPE + } + + int mark = index; // mark + String s = readAtom(); // updates index + if (s == null) + s = ""; // avoid possible NPE + if (s.equalsIgnoreCase("OK")) + type |= OK; + else if (s.equalsIgnoreCase("NO")) + type |= NO; + else if (s.equalsIgnoreCase("BAD")) + type |= BAD; + else if (s.equalsIgnoreCase("BYE")) + type |= BYE; + else + index = mark; // reset + + pindex = index; + return; + } + + public void skipSpaces() { + while (index < size && buffer[index] == ' ') + index++; + } + + /** + * Skip past any spaces. If the next non-space character is c, + * consume it and return true. Otherwise stop at that point + * and return false. + * + * @param c the character to look for + * @return true if the character is found + */ + public boolean isNextNonSpace(char c) { + skipSpaces(); + if (index < size && buffer[index] == (byte)c) { + index++; + return true; + } + return false; + } + + /** + * Skip to the next space, for use in error recovery while parsing. + */ + public void skipToken() { + while (index < size && buffer[index] != ' ') + index++; + } + + public void skip(int count) { + index += count; + } + + public byte peekByte() { + if (index < size) + return buffer[index]; + else + return 0; // XXX - how else to signal error? + } + + /** + * Return the next byte from this Statement. + * + * @return the next byte + */ + public byte readByte() { + if (index < size) + return buffer[index++]; + else + return 0; // XXX - how else to signal error? + } + + /** + * Extract an ATOM, starting at the current position. Updates + * the internal index to beyond the Atom. + * + * @return an Atom + */ + public String readAtom() { + return readDelimString(ATOM_CHAR_DELIM); + } + + /** + * Extract a string stopping at control characters or any + * character in delim. + */ + private String readDelimString(String delim) { + skipSpaces(); + + if (index >= size) // already at end of response + return null; + + int b; + int start = index; + while (index < size && ((b = (((int)buffer[index])&0xff)) >= ' ') && + delim.indexOf((char)b) < 0 && b != 0x7f) + index++; + + return toString(buffer, start, index); + } + + /** + * Read a string as an arbitrary sequence of characters, + * stopping at the delimiter Used to read part of a + * response code inside []. + * + * @param delim the delimiter character + * @return the string + */ + public String readString(char delim) { + skipSpaces(); + + if (index >= size) // already at end of response + return null; + + int start = index; + while (index < size && buffer[index] != delim) + index++; + + return toString(buffer, start, index); + } + + public String[] readStringList() { + return readStringList(false); + } + + public String[] readAtomStringList() { + return readStringList(true); + } + + private String[] readStringList(boolean atom) { + skipSpaces(); + + if (buffer[index] != '(') { // not what we expected + return null; + } + index++; // skip '(' + + // to handle buggy IMAP servers, we tolerate multiple spaces as + // well as spaces after the left paren or before the right paren + List result = new ArrayList<>(); + while (!isNextNonSpace(')')) { + String s = atom ? readAtomString() : readString(); + if (s == null) // not the expected string or atom + break; + result.add(s); + } + + return result.toArray(new String[result.size()]); + } + + /** + * Extract an integer, starting at the current position. Updates the + * internal index to beyond the number. Returns -1 if a number was + * not found. + * + * @return a number + */ + public int readNumber() { + // Skip leading spaces + skipSpaces(); + + int start = index; + while (index < size && Character.isDigit((char)buffer[index])) + index++; + + if (index > start) { + try { + return ASCIIUtility.parseInt(buffer, start, index); + } catch (NumberFormatException nex) { } + } + + return -1; + } + + /** + * Extract a long number, starting at the current position. Updates the + * internal index to beyond the number. Returns -1 if a long number + * was not found. + * + * @return a long + */ + public long readLong() { + // Skip leading spaces + skipSpaces(); + + int start = index; + while (index < size && Character.isDigit((char)buffer[index])) + index++; + + if (index > start) { + try { + return ASCIIUtility.parseLong(buffer, start, index); + } catch (NumberFormatException nex) { } + } + + return -1; + } + + /** + * Extract a NSTRING, starting at the current position. Return it as + * a String. The sequence 'NIL' is returned as null + * + * NSTRING := QuotedString | Literal | "NIL" + * + * @return a String + */ + public String readString() { + return (String)parseString(false, true); + } + + /** + * Extract a NSTRING, starting at the current position. Return it as + * a ByteArrayInputStream. The sequence 'NIL' is returned as null + * + * NSTRING := QuotedString | Literal | "NIL" + * + * @return a ByteArrayInputStream + */ + public ByteArrayInputStream readBytes() { + ByteArray ba = readByteArray(); + if (ba != null) + return ba.toByteArrayInputStream(); + else + return null; + } + + /** + * Extract a NSTRING, starting at the current position. Return it as + * a ByteArray. The sequence 'NIL' is returned as null + * + * NSTRING := QuotedString | Literal | "NIL" + * + * @return a ByteArray + */ + public ByteArray readByteArray() { + /* + * Special case, return the data after the continuation uninterpreted. + * It's usually a challenge for an AUTHENTICATE command. + */ + if (isContinuation()) { + skipSpaces(); + return new ByteArray(buffer, index, size - index); + } + return (ByteArray)parseString(false, false); + } + + /** + * Extract an ASTRING, starting at the current position + * and return as a String. An ASTRING can be a QuotedString, a + * Literal or an Atom (plus ']'). + * + * Any errors in parsing returns null + * + * ASTRING := QuotedString | Literal | 1*ASTRING_CHAR + * + * @return a String + */ + public String readAtomString() { + return (String)parseString(true, true); + } + + /** + * Generic parsing routine that can parse out a Quoted-String, + * Literal or Atom and return the parsed token as a String + * or a ByteArray. Errors or NIL data will return null. + */ + private Object parseString(boolean parseAtoms, boolean returnString) { + byte b; + + // Skip leading spaces + skipSpaces(); + + b = buffer[index]; + if (b == '"') { // QuotedString + index++; // skip the quote + int start = index; + int copyto = index; + + while (index < size && (b = buffer[index]) != '"') { + if (b == '\\') // skip escaped byte + index++; + if (index != copyto) { // only copy if we need to + // Beware: this is a destructive copy. I'm + // pretty sure this is OK, but ... ;> + buffer[copyto] = buffer[index]; + } + copyto++; + index++; + } + if (index >= size) { + // didn't find terminating quote, something is seriously wrong + //throw new ArrayIndexOutOfBoundsException( + // "index = " + index + ", size = " + size); + return null; + } else + index++; // skip past the terminating quote + + if (returnString) + return toString(buffer, start, copyto); + else + return new ByteArray(buffer, start, copyto-start); + } else if (b == '{') { // Literal + int start = ++index; // note the start position + + while (buffer[index] != '}') + index++; + + int count = 0; + try { + count = ASCIIUtility.parseInt(buffer, start, index); + } catch (NumberFormatException nex) { + // throw new ParsingException(); + return null; + } + + start = index + 3; // skip "}\r\n" + index = start + count; // position index to beyond the literal + + if (returnString) // return as String + return toString(buffer, start, start + count); + else + return new ByteArray(buffer, start, count); + } else if (parseAtoms) { // parse as ASTRING-CHARs + int start = index; // track this, so that we can use to + // creating ByteArrayInputStream below. + String s = readDelimString(ASTRING_CHAR_DELIM); + if (returnString) + return s; + else // *very* unlikely + return new ByteArray(buffer, start, index); + } else if (b == 'N' || b == 'n') { // the only valid value is 'NIL' + index += 3; // skip past NIL + return null; + } + return null; // Error + } + + private String toString(byte[] buffer, int start, int end) { + return utf8 ? + new String(buffer, start, end - start, StandardCharsets.UTF_8) : + ASCIIUtility.toString(buffer, start, end); + } + + public int getType() { + return type; + } + + public boolean isContinuation() { + return ((type & TAG_MASK) == CONTINUATION); + } + + public boolean isTagged() { + return ((type & TAG_MASK) == TAGGED); + } + + public boolean isUnTagged() { + return ((type & TAG_MASK) == UNTAGGED); + } + + public boolean isOK() { + return ((type & TYPE_MASK) == OK); + } + + public boolean isNO() { + return ((type & TYPE_MASK) == NO); + } + + public boolean isBAD() { + return ((type & TYPE_MASK) == BAD); + } + + public boolean isBYE() { + return ((type & TYPE_MASK) == BYE); + } + + public boolean isSynthetic() { + return ((type & SYNTHETIC) == SYNTHETIC); + } + + /** + * Return the tag, if this is a tagged statement. + * + * @return tag of this tagged statement + */ + public String getTag() { + return tag; + } + + /** + * Return the rest of the response as a string, usually used to + * return the arbitrary message text after a NO response. + * + * @return the rest of the response + */ + public String getRest() { + skipSpaces(); + return toString(buffer, index, size); + } + + /** + * Return the exception for a synthetic BYE response. + * + * @return the exception + * @since JavaMail 1.5.4 + */ + public Exception getException() { + return ex; + } + + /** + * Reset pointer to beginning of response. + */ + public void reset() { + index = pindex; + } + + @Override + public String toString() { + return toString(buffer, 0, size); + } + +} diff --git a/app/src/main/java/com/sun/mail/iap/ResponseHandler.java b/app/src/main/java/com/sun/mail/iap/ResponseHandler.java new file mode 100644 index 0000000000..7d5a61c822 --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/ResponseHandler.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +/** + * This class + * + * @author John Mani + */ + +public interface ResponseHandler { + public void handleResponse(Response r); +} diff --git a/app/src/main/java/com/sun/mail/iap/ResponseInputStream.java b/app/src/main/java/com/sun/mail/iap/ResponseInputStream.java new file mode 100644 index 0000000000..9e05b18f1f --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/ResponseInputStream.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +import java.io.*; +import com.sun.mail.iap.ByteArray; +import com.sun.mail.util.ASCIIUtility; + +/** + * + * Inputstream that is used to read a Response. + * + * @author Arun Krishnan + * @author Bill Shannon + */ + +public class ResponseInputStream { + + private static final int minIncrement = 256; + private static final int maxIncrement = 256 * 1024; + private static final int incrementSlop = 16; + + // where we read from + private BufferedInputStream bin; + + /** + * Constructor. + * + * @param in the InputStream to wrap + */ + public ResponseInputStream(InputStream in) { + bin = new BufferedInputStream(in, 2 * 1024); + } + + /** + * Read a Response from the InputStream. + * + * @return ByteArray that contains the Response + * @exception IOException for I/O errors + */ + public ByteArray readResponse() throws IOException { + return readResponse(null); + } + + /** + * Read a Response from the InputStream. + * + * @param ba the ByteArray in which to store the response, or null + * @return ByteArray that contains the Response + * @exception IOException for I/O errors + */ + public ByteArray readResponse(ByteArray ba) throws IOException { + if (ba == null) + ba = new ByteArray(new byte[128], 0, 128); + + byte[] buffer = ba.getBytes(); + int idx = 0; + for (;;) { // read until CRLF with no preceeding literal + // XXX - b needs to be an int, to handle bytes with value 0xff + int b = 0; + boolean gotCRLF=false; + + // Read a CRLF terminated line from the InputStream + while (!gotCRLF && + ((b = bin.read()) != -1)) { + if (b == '\n') { + if ((idx > 0) && buffer[idx-1] == '\r') + gotCRLF = true; + } + if (idx >= buffer.length) { + int incr = buffer.length; + if (incr > maxIncrement) + incr = maxIncrement; + ba.grow(incr); + buffer = ba.getBytes(); + } + buffer[idx++] = (byte)b; + } + + if (b == -1) + throw new IOException("Connection dropped by server?"); + + // Now lets check for literals : {}CRLF + // Note: index needs to >= 5 for the above sequence to occur + if (idx < 5 || buffer[idx-3] != '}') + break; + + int i; + // look for left curly + for (i = idx - 4; i >= 0; i--) + if (buffer[i] == '{') + break; + + if (i < 0) // Nope, not a literal ? + break; + + int count = 0; + // OK, handle the literal .. + try { + count = ASCIIUtility.parseInt(buffer, i+1, idx-3); + } catch (NumberFormatException e) { + break; + } + + // Now read 'count' bytes. (Note: count could be 0) + if (count > 0) { + int avail = buffer.length - idx; // available space in buffer + if (count + incrementSlop > avail) { + // need count-avail more bytes + ba.grow(minIncrement > count + incrementSlop - avail ? + minIncrement : count + incrementSlop - avail); + buffer = ba.getBytes(); + } + + /* + * read() might not return all the bytes in one shot, + * so call repeatedly till we are done + */ + int actual; + while (count > 0) { + actual = bin.read(buffer, idx, count); + if (actual == -1) + throw new IOException("Connection dropped by server?"); + count -= actual; + idx += actual; + } + } + // back to top of loop to read until CRLF + } + ba.setCount(idx); + return ba; + } + + /** + * How much buffered data do we have? + * + * @return number of bytes available + * @exception IOException if the stream has been closed + * @since JavaMail 1.5.4 + */ + public int available() throws IOException { + return bin.available(); + } +} diff --git a/app/src/main/java/com/sun/mail/iap/package.html b/app/src/main/java/com/sun/mail/iap/package.html new file mode 100644 index 0000000000..2607e33a2e --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/package.html @@ -0,0 +1,33 @@ + + + + + + +com.sun.mail.iap package + + + +

+This package includes internal IMAP support classes and +SHOULD NOT BE USED DIRECTLY BY APPLICATIONS. +

+ + + diff --git a/app/src/main/java/com/sun/mail/imap/ACL.java b/app/src/main/java/com/sun/mail/imap/ACL.java new file mode 100644 index 0000000000..ee99c95e2c --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/ACL.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.util.*; + +/** + * An access control list entry for a particular authentication identifier + * (user or group). Associates a set of Rights with the identifier. + * See RFC 2086. + *

+ * + * @author Bill Shannon + */ + +public class ACL implements Cloneable { + + private String name; + private Rights rights; + + /** + * Construct an ACL entry for the given identifier and with no rights. + * + * @param name the identifier name + */ + public ACL(String name) { + this.name = name; + this.rights = new Rights(); + } + + /** + * Construct an ACL entry for the given identifier with the given rights. + * + * @param name the identifier name + * @param rights the rights + */ + public ACL(String name, Rights rights) { + this.name = name; + this.rights = rights; + } + + /** + * Get the identifier name for this ACL entry. + * + * @return the identifier name + */ + public String getName() { + return name; + } + + /** + * Set the rights associated with this ACL entry. + * + * @param rights the rights + */ + public void setRights(Rights rights) { + this.rights = rights; + } + + /** + * Get the rights associated with this ACL entry. + * Returns the actual Rights object referenced by this ACL; + * modifications to the Rights object will effect this ACL. + * + * @return the rights + */ + public Rights getRights() { + return rights; + } + + /** + * Clone this ACL entry. + */ + @Override + public Object clone() throws CloneNotSupportedException { + ACL acl = (ACL)super.clone(); + acl.rights = (Rights)this.rights.clone(); + return acl; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/AppendUID.java b/app/src/main/java/com/sun/mail/imap/AppendUID.java new file mode 100644 index 0000000000..56c77fed82 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/AppendUID.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import com.sun.mail.iap.*; + +/** + * Information from the APPENDUID response code + * defined by the UIDPLUS extension - + * RFC 4315. + * + * @author Bill Shannon + */ + +public class AppendUID { + public long uidvalidity = -1; + public long uid = -1; + + public AppendUID(long uidvalidity, long uid) { + this.uidvalidity = uidvalidity; + this.uid = uid; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/CopyUID.java b/app/src/main/java/com/sun/mail/imap/CopyUID.java new file mode 100644 index 0000000000..3648fc5cb2 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/CopyUID.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import com.sun.mail.imap.protocol.UIDSet; + +/** + * Information from the COPYUID response code + * defined by the UIDPLUS extension - + * RFC 4315. + * + * @author Bill Shannon + */ + +public class CopyUID { + public long uidvalidity = -1; + public UIDSet[] src; + public UIDSet[] dst; + + public CopyUID(long uidvalidity, UIDSet[] src, UIDSet[] dst) { + this.uidvalidity = uidvalidity; + this.src = src; + this.dst = dst; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/DefaultFolder.java b/app/src/main/java/com/sun/mail/imap/DefaultFolder.java new file mode 100644 index 0000000000..9dab04fb5a --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/DefaultFolder.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.MethodNotSupportedException; +import com.sun.mail.iap.ProtocolException; +import com.sun.mail.imap.protocol.IMAPProtocol; +import com.sun.mail.imap.protocol.ListInfo; + +/** + * The default IMAP folder (root of the naming hierarchy). + * + * @author John Mani + */ + +public class DefaultFolder extends IMAPFolder { + + protected DefaultFolder(IMAPStore store) { + super("", UNKNOWN_SEPARATOR, store, null); + exists = true; // of course + type = HOLDS_FOLDERS; // obviously + } + + @Override + public synchronized String getName() { + return fullName; + } + + @Override + public Folder getParent() { + return null; + } + + @Override + public synchronized Folder[] list(final String pattern) + throws MessagingException { + ListInfo[] li = null; + + li = (ListInfo[])doCommand(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) throws ProtocolException { + return p.list("", pattern); + } + }); + + if (li == null) + return new Folder[0]; + + IMAPFolder[] folders = new IMAPFolder[li.length]; + for (int i = 0; i < folders.length; i++) + folders[i] = ((IMAPStore)store).newIMAPFolder(li[i]); + return folders; + } + + @Override + public synchronized Folder[] listSubscribed(final String pattern) + throws MessagingException { + ListInfo[] li = null; + + li = (ListInfo[])doCommand(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) throws ProtocolException { + return p.lsub("", pattern); + } + }); + + if (li == null) + return new Folder[0]; + + IMAPFolder[] folders = new IMAPFolder[li.length]; + for (int i = 0; i < folders.length; i++) + folders[i] = ((IMAPStore)store).newIMAPFolder(li[i]); + return folders; + } + + @Override + public boolean hasNewMessages() throws MessagingException { + // Not applicable on DefaultFolder + return false; + } + + @Override + public Folder getFolder(String name) throws MessagingException { + return ((IMAPStore)store).newIMAPFolder(name, UNKNOWN_SEPARATOR); + } + + @Override + public boolean delete(boolean recurse) throws MessagingException { + // Not applicable on DefaultFolder + throw new MethodNotSupportedException("Cannot delete Default Folder"); + } + + @Override + public boolean renameTo(Folder f) throws MessagingException { + // Not applicable on DefaultFolder + throw new MethodNotSupportedException("Cannot rename Default Folder"); + } + + @Override + public void appendMessages(Message[] msgs) throws MessagingException { + // Not applicable on DefaultFolder + throw new MethodNotSupportedException("Cannot append to Default Folder"); + } + + @Override + public Message[] expunge() throws MessagingException { + // Not applicable on DefaultFolder + throw new MethodNotSupportedException("Cannot expunge Default Folder"); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IMAPBodyPart.java b/app/src/main/java/com/sun/mail/imap/IMAPBodyPart.java new file mode 100644 index 0000000000..181bb94512 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IMAPBodyPart.java @@ -0,0 +1,454 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.*; + +import java.util.Enumeration; +import javax.mail.*; +import javax.mail.internet.*; +import javax.activation.*; + +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.ReadableMime; +import com.sun.mail.util.LineOutputStream; +import com.sun.mail.util.SharedByteArrayOutputStream; +import com.sun.mail.iap.*; +import com.sun.mail.imap.protocol.*; + +/** + * An IMAP body part. + * + * @author John Mani + * @author Bill Shannon + */ + +public class IMAPBodyPart extends MimeBodyPart implements ReadableMime { + private IMAPMessage message; + private BODYSTRUCTURE bs; + private String sectionId; + + // processed values .. + private String type; + private String description; + + private boolean headersLoaded = false; + + private static final boolean decodeFileName = + PropUtil.getBooleanSystemProperty("mail.mime.decodefilename", false); + + protected IMAPBodyPart(BODYSTRUCTURE bs, String sid, IMAPMessage message) { + super(); + this.bs = bs; + this.sectionId = sid; + this.message = message; + // generate content-type + ContentType ct = new ContentType(bs.type, bs.subtype, bs.cParams); + type = ct.toString(); + } + + /* Override this method to make it a no-op, rather than throw + * an IllegalWriteException. This will permit IMAPBodyParts to + * be inserted in newly crafted MimeMessages, especially when + * forwarding or replying to messages. + */ + @Override + protected void updateHeaders() { + return; + } + + @Override + public int getSize() throws MessagingException { + return bs.size; + } + + @Override + public int getLineCount() throws MessagingException { + return bs.lines; + } + + @Override + public String getContentType() throws MessagingException { + return type; + } + + @Override + public String getDisposition() throws MessagingException { + return bs.disposition; + } + + @Override + public void setDisposition(String disposition) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public String getEncoding() throws MessagingException { + return bs.encoding; + } + + @Override + public String getContentID() throws MessagingException { + return bs.id; + } + + @Override + public String getContentMD5() throws MessagingException { + return bs.md5; + } + + @Override + public void setContentMD5(String md5) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public String getDescription() throws MessagingException { + if (description != null) // cached value ? + return description; + + if (bs.description == null) + return null; + + try { + description = MimeUtility.decodeText(bs.description); + } catch (UnsupportedEncodingException ex) { + description = bs.description; + } + + return description; + } + + @Override + public void setDescription(String description, String charset) + throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public String getFileName() throws MessagingException { + String filename = null; + if (bs.dParams != null) + filename = bs.dParams.get("filename"); + if ((filename == null || filename.isEmpty()) && bs.cParams != null) + filename = bs.cParams.get("name"); + if (decodeFileName && filename != null) { + try { + filename = MimeUtility.decodeText(filename); + } catch (UnsupportedEncodingException ex) { + throw new MessagingException("Can't decode filename", ex); + } + } + return filename; + } + + @Override + public void setFileName(String filename) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + protected InputStream getContentStream() throws MessagingException { + InputStream is = null; + boolean pk = message.getPeek(); // acquire outside of message cache lock + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(message.getMessageCacheLock()) { + try { + IMAPProtocol p = message.getProtocol(); + + // Check whether this message is expunged + message.checkExpunged(); + + if (p.isREV1() && (message.getFetchBlockSize() != -1)) + return new IMAPInputStream(message, sectionId, + message.ignoreBodyStructureSize() ? -1 : bs.size, pk); + + // Else, vanila IMAP4, no partial fetch + + int seqnum = message.getSequenceNumber(); + BODY b; + if (pk) + b = p.peekBody(seqnum, sectionId); + else + b = p.fetchBody(seqnum, sectionId); + if (b != null) + is = b.getByteArrayInputStream(); + } catch (ConnectionException cex) { + throw new FolderClosedException( + message.getFolder(), cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + if (is == null) { + message.forceCheckExpunged(); // may throw MessageRemovedException + // nope, the server doesn't think it's expunged. + // can't tell the difference between the server returning NIL + // and some other error that caused null to be returned above, + // so we'll just assume it was empty content. + is = new ByteArrayInputStream(new byte[0]); + } + return is; + } + + /** + * Return the MIME format stream of headers for this body part. + */ + private InputStream getHeaderStream() throws MessagingException { + if (!message.isREV1()) + loadHeaders(); // will be needed below + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(message.getMessageCacheLock()) { + try { + IMAPProtocol p = message.getProtocol(); + + // Check whether this message got expunged + message.checkExpunged(); + + if (p.isREV1()) { + int seqnum = message.getSequenceNumber(); + BODY b = p.peekBody(seqnum, sectionId + ".MIME"); + + if (b == null) + throw new MessagingException("Failed to fetch headers"); + + ByteArrayInputStream bis = b.getByteArrayInputStream(); + if (bis == null) + throw new MessagingException("Failed to fetch headers"); + return bis; + + } else { + // Can't read it from server, have to fake it + SharedByteArrayOutputStream bos = + new SharedByteArrayOutputStream(0); + LineOutputStream los = new LineOutputStream(bos); + + try { + // Write out the header + Enumeration hdrLines + = super.getAllHeaderLines(); + while (hdrLines.hasMoreElements()) + los.writeln(hdrLines.nextElement()); + + // The CRLF separator between header and content + los.writeln(); + } catch (IOException ioex) { + // should never happen + } finally { + try { + los.close(); + } catch (IOException cex) { } + } + return bos.toStream(); + } + } catch (ConnectionException cex) { + throw new FolderClosedException( + message.getFolder(), cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } + + /** + * Return the MIME format stream corresponding to this message part. + * + * @return the MIME format stream + * @since JavaMail 1.4.5 + */ + @Override + public InputStream getMimeStream() throws MessagingException { + /* + * The IMAP protocol doesn't support returning the entire + * part content in one operation so we have to fake it by + * concatenating the header stream and the content stream. + */ + return new SequenceInputStream(getHeaderStream(), getContentStream()); + } + + @Override + public synchronized DataHandler getDataHandler() + throws MessagingException { + if (dh == null) { + if (bs.isMulti()) + dh = new DataHandler( + new IMAPMultipartDataSource( + this, bs.bodies, sectionId, message) + ); + else if (bs.isNested() && message.isREV1() && bs.envelope != null) + dh = new DataHandler( + new IMAPNestedMessage(message, + bs.bodies[0], + bs.envelope, + sectionId), + type + ); + } + + return super.getDataHandler(); + } + + @Override + public void setDataHandler(DataHandler content) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public void setContent(Object o, String type) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public void setContent(Multipart mp) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public String[] getHeader(String name) throws MessagingException { + loadHeaders(); + return super.getHeader(name); + } + + @Override + public void setHeader(String name, String value) + throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public void addHeader(String name, String value) + throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public void removeHeader(String name) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public Enumeration

getAllHeaders() throws MessagingException { + loadHeaders(); + return super.getAllHeaders(); + } + + @Override + public Enumeration
getMatchingHeaders(String[] names) + throws MessagingException { + loadHeaders(); + return super.getMatchingHeaders(names); + } + + @Override + public Enumeration
getNonMatchingHeaders(String[] names) + throws MessagingException { + loadHeaders(); + return super.getNonMatchingHeaders(names); + } + + @Override + public void addHeaderLine(String line) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public Enumeration getAllHeaderLines() throws MessagingException { + loadHeaders(); + return super.getAllHeaderLines(); + } + + @Override + public Enumeration getMatchingHeaderLines(String[] names) + throws MessagingException { + loadHeaders(); + return super.getMatchingHeaderLines(names); + } + + @Override + public Enumeration getNonMatchingHeaderLines(String[] names) + throws MessagingException { + loadHeaders(); + return super.getNonMatchingHeaderLines(names); + } + + private synchronized void loadHeaders() throws MessagingException { + if (headersLoaded) + return; + + // "headers" should never be null since it's set in the constructor. + // If something did go wrong this will fix it, but is an unsynchronized + // assignment of "headers". + if (headers == null) + headers = new InternetHeaders(); + + // load headers + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(message.getMessageCacheLock()) { + try { + IMAPProtocol p = message.getProtocol(); + + // Check whether this message got expunged + message.checkExpunged(); + + if (p.isREV1()) { + int seqnum = message.getSequenceNumber(); + BODY b = p.peekBody(seqnum, sectionId + ".MIME"); + + if (b == null) + throw new MessagingException("Failed to fetch headers"); + + ByteArrayInputStream bis = b.getByteArrayInputStream(); + if (bis == null) + throw new MessagingException("Failed to fetch headers"); + + headers.load(bis); + + } else { + + // RFC 1730 does not provide for fetching BodyPart headers + // So, just dump the RFC1730 BODYSTRUCTURE into the + // headerStore + + // Content-Type + headers.addHeader("Content-Type", type); + // Content-Transfer-Encoding + headers.addHeader("Content-Transfer-Encoding", bs.encoding); + // Content-Description + if (bs.description != null) + headers.addHeader("Content-Description", + bs.description); + // Content-ID + if (bs.id != null) + headers.addHeader("Content-ID", bs.id); + // Content-MD5 + if (bs.md5 != null) + headers.addHeader("Content-MD5", bs.md5); + } + } catch (ConnectionException cex) { + throw new FolderClosedException( + message.getFolder(), cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + headersLoaded = true; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IMAPFolder.java b/app/src/main/java/com/sun/mail/imap/IMAPFolder.java new file mode 100644 index 0000000000..74f2f56b96 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IMAPFolder.java @@ -0,0 +1,4153 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.util.Date; +import java.util.Vector; +import java.util.Hashtable; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Locale; +import java.util.logging.Level; +import java.io.*; +import java.net.SocketTimeoutException; +import java.nio.channels.SocketChannel; + +import javax.mail.*; +import javax.mail.event.*; +import javax.mail.internet.*; +import javax.mail.search.*; + +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.CRLFOutputStream; +import com.sun.mail.iap.*; +import com.sun.mail.imap.protocol.*; + +/** + * This class implements an IMAP folder.

+ * + * A closed IMAPFolder object shares a protocol connection with its IMAPStore + * object. When the folder is opened, it gets its own protocol connection.

+ * + * Applications that need to make use of IMAP-specific features may cast + * a Folder object to an IMAPFolder object and + * use the methods on this class.

+ * + * The {@link #getQuota getQuota} and + * {@link #setQuota setQuota} methods support the IMAP QUOTA extension. + * Refer to RFC 2087 + * for more information.

+ * + * The {@link #getACL getACL}, {@link #addACL addACL}, + * {@link #removeACL removeACL}, {@link #addRights addRights}, + * {@link #removeRights removeRights}, {@link #listRights listRights}, and + * {@link #myRights myRights} methods support the IMAP ACL extension. + * Refer to RFC 2086 + * for more information.

+ * + * The {@link #getSortedMessages getSortedMessages} + * methods support the IMAP SORT extension. + * Refer to RFC 5256 + * for more information.

+ * + * The {@link #open(int,com.sun.mail.imap.ResyncData) open(int,ResyncData)} + * method and {@link com.sun.mail.imap.ResyncData ResyncData} class supports + * the IMAP CONDSTORE and QRESYNC extensions. + * Refer to RFC 4551 + * and RFC 5162 + * for more information.

+ * + * The {@link #doCommand doCommand} method and + * {@link IMAPFolder.ProtocolCommand IMAPFolder.ProtocolCommand} + * interface support use of arbitrary IMAP protocol commands.

+ * + * See the com.sun.mail.imap package + * documentation for further information on the IMAP protocol provider.

+ * + * WARNING: The APIs unique to this class should be + * considered EXPERIMENTAL. They may be changed in the + * future in ways that are incompatible with applications using the + * current APIs. + * + * @author John Mani + * @author Bill Shannon + * @author Jim Glennon + */ + +/* + * The folder object itself serves as a lock for the folder's state + * EXCEPT for the message cache (see below), typically by using + * synchronized methods. When checking that a folder is open or + * closed, the folder's lock must be held. It's important that the + * folder's lock is acquired before the messageCacheLock (see below). + * Thus, the locking hierarchy is that the folder lock, while optional, + * must be acquired before the messageCacheLock, if it's acquired at + * all. Be especially careful of callbacks that occur while holding + * the messageCacheLock into (e.g.) superclass Folder methods that are + * synchronized. Note that methods in IMAPMessage will acquire the + * messageCacheLock without acquiring the folder lock.

+ * + * When a folder is opened, it creates a messageCache (a Vector) of + * empty IMAPMessage objects. Each Message has a messageNumber - which + * is its index into the messageCache, and a sequenceNumber - which is + * its IMAP sequence-number. All operations on a Message which involve + * communication with the server, use the message's sequenceNumber.

+ * + * The most important thing to note here is that the server can send + * unsolicited EXPUNGE notifications as part of the responses for "most" + * commands. Refer RFC 3501, sections 5.3 & 5.5 for gory details. Also, + * the server sends these notifications AFTER the message has been + * expunged. And once a message is expunged, the sequence-numbers of + * those messages after the expunged one are renumbered. This essentially + * means that the mapping between *any* Message and its sequence-number + * can change in the period when a IMAP command is issued and its responses + * are processed. Hence we impose a strict locking model as follows:

+ * + * We define one mutex per folder - this is just a Java Object (named + * messageCacheLock). Any time a command is to be issued to the IMAP + * server (i.e., anytime the corresponding IMAPProtocol method is + * invoked), follow the below style: + * + * synchronized (messageCacheLock) { // ACQUIRE LOCK + * issue command () + * + * // The response processing is typically done within + * // the handleResponse() callback. A few commands (Fetch, + * // Expunge) return *all* responses and hence their + * // processing is done here itself. Now, as part of the + * // processing unsolicited EXPUNGE responses, we renumber + * // the necessary sequence-numbers. Thus the renumbering + * // happens within this critical-region, surrounded by + * // locks. + * process responses () + * } // RELEASE LOCK + * + * This technique is used both by methods in IMAPFolder and by methods + * in IMAPMessage and other classes that operate on data in the folder. + * Note that holding the messageCacheLock has the side effect of + * preventing the folder from being closed, and thus ensuring that the + * folder's protocol object is still valid. The protocol object should + * only be accessed while holding the messageCacheLock (except for calls + * to IMAPProtocol.isREV1(), which don't need to be protected because it + * doesn't access the server). + * + * Note that interactions with the Store's protocol connection do + * not have to be protected as above, since the Store's protocol is + * never in a "meaningful" SELECT-ed state. + */ + +public class IMAPFolder extends Folder implements UIDFolder, ResponseHandler { + + protected volatile String fullName; // full name + protected String name; // name + protected int type; // folder type. + protected char separator; // separator + protected Flags availableFlags; // available flags + protected Flags permanentFlags; // permanent flags + protected volatile boolean exists; // whether this folder really exists ? + protected boolean isNamespace = false; // folder is a namespace name + protected volatile String[] attributes;// name attributes from LIST response + + protected volatile IMAPProtocol protocol; // this folder's protocol object + protected MessageCache messageCache;// message cache + // accessor lock for message cache + protected final Object messageCacheLock = new Object(); + + protected Hashtable uidTable; // UID->Message hashtable + + /* An IMAP delimiter is a 7bit US-ASCII character. (except NUL). + * We use '\uffff' (a non 7bit character) to indicate that we havent + * yet determined what the separator character is. + * We use '\u0000' (NUL) to indicate that no separator character + * exists, i.e., a flat hierarchy + */ + static final protected char UNKNOWN_SEPARATOR = '\uffff'; + + private volatile boolean opened = false; // is this folder opened ? + + /* This field tracks the state of this folder. If the folder is closed + * due to external causes (i.e, not thru the close() method), then + * this field will remain false. If the folder is closed thru the + * close() method, then this field is set to true. + * + * If reallyClosed is false, then a FolderClosedException is + * generated when a method is invoked on any Messaging object + * owned by this folder. If reallyClosed is true, then the + * IllegalStateException runtime exception is thrown. + */ + private boolean reallyClosed = true; + + /* + * The idleState field supports the IDLE command. + * Normally when executing an IMAP command we hold the + * messageCacheLock and often the folder lock (see above). + * While executing the IDLE command we can't hold either + * of these locks or it would prevent other threads from + * entering Folder methods even far enough to check whether + * an IDLE command is in progress. We need to check before + * issuing another command so that we can abort the IDLE + * command. + * + * The idleState field is protected by the messageCacheLock. + * The RUNNING state is the normal state and means no IDLE + * command is in progress. The IDLE state means we've issued + * an IDLE command and are reading responses. The ABORTING + * state means we've sent the DONE continuation command and + * are waiting for the thread running the IDLE command to + * break out of its read loop. + * + * When an IDLE command is in progress, the thread calling + * the idle method will be reading from the IMAP connection + * while holding neither the folder lock nor the messageCacheLock. + * It's obviously critical that no other thread try to send a + * command or read from the connection while in this state. + * However, other threads can send the DONE continuation + * command that will cause the server to break out of the IDLE + * loop and send the ending tag response to the IDLE command. + * The thread in the idle method that's reading the responses + * from the IDLE command will see this ending response and + * complete the idle method, setting the idleState field back + * to RUNNING, and notifying any threads waiting to use the + * connection. + * + * All uses of the IMAP connection (IMAPProtocol object) must + * be done while holding the messageCacheLock and must be + * preceeded by a check to make sure an IDLE command is not + * running, and abort the IDLE command if necessary. While + * waiting for the IDLE command to complete, these other threads + * will give up the messageCacheLock, but might still be holding + * the folder lock. This check is done by the getProtocol() + * method, resulting in a typical usage pattern of: + * + * synchronized (messageCacheLock) { + * IMAPProtocol p = getProtocol(); // may block waiting for IDLE + * // ... use protocol + * } + */ + private static final int RUNNING = 0; // not doing IDLE command + private static final int IDLE = 1; // IDLE command in effect + private static final int ABORTING = 2; // IDLE command aborting + private int idleState = RUNNING; + private IdleManager idleManager; + + private volatile int total = -1; // total number of messages in the + // message cache + private volatile int recent = -1; // number of recent messages + private int realTotal = -1; // total number of messages on + // the server + private long uidvalidity = -1; // UIDValidity + private long uidnext = -1; // UIDNext + private boolean uidNotSticky = false; // RFC 4315 + private volatile long highestmodseq = -1; // RFC 4551 - CONDSTORE + private boolean doExpungeNotification = true; // used in expunge handler + + private Status cachedStatus = null; + private long cachedStatusTime = 0; + + private boolean hasMessageCountListener = false; // optimize notification + + protected MailLogger logger; + private MailLogger connectionPoolLogger; + + /** + * A fetch profile item for fetching headers. + * This inner class extends the FetchProfile.Item + * class to add new FetchProfile item types, specific to IMAPFolders. + * + * @see FetchProfile + */ + public static class FetchProfileItem extends FetchProfile.Item { + protected FetchProfileItem(String name) { + super(name); + } + + /** + * HEADERS is a fetch profile item that can be included in a + * FetchProfile during a fetch request to a Folder. + * This item indicates that the headers for messages in the specified + * range are desired to be prefetched.

+ * + * An example of how a client uses this is below: + *

+	 *
+	 * 	FetchProfile fp = new FetchProfile();
+	 *	fp.add(IMAPFolder.FetchProfileItem.HEADERS);
+	 *	folder.fetch(msgs, fp);
+	 *
+	 * 
+ */ + public static final FetchProfileItem HEADERS = + new FetchProfileItem("HEADERS"); + + /** + * SIZE is a fetch profile item that can be included in a + * FetchProfile during a fetch request to a Folder. + * This item indicates that the sizes of the messages in the specified + * range are desired to be prefetched.

+ * + * SIZE was moved to FetchProfile.Item in JavaMail 1.5. + * + * @deprecated + */ + @Deprecated + public static final FetchProfileItem SIZE = + new FetchProfileItem("SIZE"); + + /** + * MESSAGE is a fetch profile item that can be included in a + * FetchProfile during a fetch request to a Folder. + * This item indicates that the entire messages (headers and body, + * including all "attachments") in the specified + * range are desired to be prefetched. Note that the entire message + * content is cached in memory while the Folder is open. The cached + * message will be parsed locally to return header information and + * message content.

+ * + * An example of how a client uses this is below: + *

+	 *
+	 * 	FetchProfile fp = new FetchProfile();
+	 *	fp.add(IMAPFolder.FetchProfileItem.MESSAGE);
+	 *	folder.fetch(msgs, fp);
+	 *
+	 * 
+ * + * @since JavaMail 1.5.2 + */ + public static final FetchProfileItem MESSAGE = + new FetchProfileItem("MESSAGE"); + + /** + * INTERNALDATE is a fetch profile item that can be included in a + * FetchProfile during a fetch request to a Folder. + * This item indicates that the IMAP INTERNALDATE values + * (received date) of the messages in the specified + * range are desired to be prefetched.

+ * + * An example of how a client uses this is below: + *

+	 *
+	 * 	FetchProfile fp = new FetchProfile();
+	 *	fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE);
+	 *	folder.fetch(msgs, fp);
+	 *
+	 * 
+ * + * @since JavaMail 1.5.5 + */ + public static final FetchProfileItem INTERNALDATE = + new FetchProfileItem("INTERNALDATE"); + } + + /** + * Constructor used to create a possibly non-existent folder. + * + * @param fullName fullname of this folder + * @param separator the default separator character for this + * folder's namespace + * @param store the Store + * @param isNamespace if this folder represents a namespace + */ + protected IMAPFolder(String fullName, char separator, IMAPStore store, + Boolean isNamespace) { + super(store); + if (fullName == null) + throw new NullPointerException("Folder name is null"); + this.fullName = fullName; + this.separator = separator; + logger = new MailLogger(this.getClass(), "DEBUG IMAP", + store.getSession().getDebug(), store.getSession().getDebugOut()); + connectionPoolLogger = store.getConnectionPoolLogger(); + + /* + * Work around apparent bug in Exchange. Exchange + * will return a name of "Public Folders/" from + * LIST "%". + * + * If name has one separator, and it's at the end, + * assume this is a namespace name and treat it + * accordingly. Usually this will happen as a result + * of the list method, but this also allows getFolder + * to work with namespace names. + */ + this.isNamespace = false; + if (separator != UNKNOWN_SEPARATOR && separator != '\0') { + int i = this.fullName.indexOf(separator); + if (i > 0 && i == this.fullName.length() - 1) { + this.fullName = this.fullName.substring(0, i); + this.isNamespace = true; + } + } + + // if we were given a value, override default chosen above + if (isNamespace != null) + this.isNamespace = isNamespace.booleanValue(); + } + + /** + * Constructor used to create an existing folder. + * + * @param li the ListInfo for this folder + * @param store the store containing this folder + */ + protected IMAPFolder(ListInfo li, IMAPStore store) { + this(li.name, li.separator, store, null); + + if (li.hasInferiors) + type |= HOLDS_FOLDERS; + if (li.canOpen) + type |= HOLDS_MESSAGES; + exists = true; + attributes = li.attrs; + } + + /* + * Ensure that this folder exists. If 'exists' has been set to true, + * we don't attempt to validate it with the server again. Note that + * this can result in a possible loss of sync with the server. + * ASSERT: Must be called with this folder's synchronization lock held. + */ + protected void checkExists() throws MessagingException { + // If the boolean field 'exists' is false, check with the + // server by invoking exists() .. + if (!exists && !exists()) + throw new FolderNotFoundException( + this, fullName + " not found"); + } + + /* + * Ensure the folder is closed. + * ASSERT: Must be called with this folder's synchronization lock held. + */ + protected void checkClosed() { + if (opened) + throw new IllegalStateException( + "This operation is not allowed on an open folder" + ); + } + + /* + * Ensure the folder is open. + * ASSERT: Must be called with this folder's synchronization lock held. + */ + protected void checkOpened() throws FolderClosedException { + assert Thread.holdsLock(this); + if (!opened) { + if (reallyClosed) + throw new IllegalStateException( + "This operation is not allowed on a closed folder" + ); + else // Folder was closed "implicitly" + throw new FolderClosedException(this, + "Lost folder connection to server" + ); + } + } + + /* + * Check that the given message number is within the range + * of messages present in this folder. If the message + * number is out of range, we ping the server to obtain any + * pending new message notifications from the server. + */ + protected void checkRange(int msgno) throws MessagingException { + if (msgno < 1) // message-numbers start at 1 + throw new IndexOutOfBoundsException("message number < 1"); + + if (msgno <= total) + return; + + // Out of range, let's ping the server and see if + // the server has more messages for us. + + synchronized(messageCacheLock) { // Acquire lock + try { + keepConnectionAlive(false); + } catch (ConnectionException cex) { + // Oops, lost connection + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } // Release lock + + if (msgno > total) // Still out of range ? Throw up ... + throw new IndexOutOfBoundsException(msgno + " > " + total); + } + + /* + * Check whether the given flags are supported by this server, + * and also verify that the folder allows setting flags. + */ + private void checkFlags(Flags flags) throws MessagingException { + assert Thread.holdsLock(this); + if (mode != READ_WRITE) + throw new IllegalStateException( + "Cannot change flags on READ_ONLY folder: " + fullName + ); + /* + if (!availableFlags.contains(flags)) + throw new MessagingException( + "These flags are not supported by this implementation" + ); + */ + } + + /** + * Get the name of this folder. + */ + @Override + public synchronized String getName() { + /* Return the last component of this Folder's full name. + * Folder components are delimited by the separator character. + */ + if (name == null) { + try { + name = fullName.substring( + fullName.lastIndexOf(getSeparator()) + 1 + ); + } catch (MessagingException mex) { } + } + return name; + } + + /** + * Get the fullname of this folder. + */ + @Override + public String getFullName() { + return fullName; + } + + /** + * Get this folder's parent. + */ + @Override + public synchronized Folder getParent() throws MessagingException { + char c = getSeparator(); + int index; + if ((index = fullName.lastIndexOf(c)) != -1) + return ((IMAPStore)store).newIMAPFolder( + fullName.substring(0, index), c); + else + return new DefaultFolder((IMAPStore)store); + } + + /** + * Check whether this folder really exists on the server. + */ + @Override + public synchronized boolean exists() throws MessagingException { + // Check whether this folder exists .. + ListInfo[] li = null; + final String lname; + if (isNamespace && separator != '\0') + lname = fullName + separator; + else + lname = fullName; + + li = (ListInfo[])doCommand(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) throws ProtocolException { + return p.list("", lname); + } + }); + + if (li != null) { + int i = findName(li, lname); + fullName = li[i].name; + separator = li[i].separator; + int len = fullName.length(); + if (separator != '\0' && len > 0 && + fullName.charAt(len - 1) == separator) { + fullName = fullName.substring(0, len - 1); + } + type = 0; + if (li[i].hasInferiors) + type |= HOLDS_FOLDERS; + if (li[i].canOpen) + type |= HOLDS_MESSAGES; + exists = true; + attributes = li[i].attrs; + } else { + exists = opened; + attributes = null; + } + + return exists; + } + + /** + * Which entry in li matches lname? + * If the name contains wildcards, more than one entry may be + * returned. + */ + private int findName(ListInfo[] li, String lname) { + int i; + // if the name contains a wildcard, there might be more than one + for (i = 0; i < li.length; i++) { + if (li[i].name.equals(lname)) + break; + } + if (i >= li.length) { // nothing matched exactly + // XXX - possibly should fail? But what if server + // is case insensitive and returns the preferred + // case of the name here? + i = 0; // use first one + } + return i; + } + + /** + * List all subfolders matching the specified pattern. + */ + @Override + public Folder[] list(String pattern) throws MessagingException { + return doList(pattern, false); + } + + /** + * List all subscribed subfolders matching the specified pattern. + */ + @Override + public Folder[] listSubscribed(String pattern) throws MessagingException { + return doList(pattern, true); + } + + private synchronized Folder[] doList(final String pattern, + final boolean subscribed) throws MessagingException { + checkExists(); // insure that this folder does exist. + + // Why waste a roundtrip to the server? + if (attributes != null && !isDirectory()) + return new Folder[0]; + + final char c = getSeparator(); + + ListInfo[] li = (ListInfo[])doCommandIgnoreFailure( + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + if (subscribed) + return p.lsub("", fullName + c + pattern); + else + return p.list("", fullName + c + pattern); + } + }); + + if (li == null) + return new Folder[0]; + + /* + * The UW based IMAP4 servers (e.g. SIMS2.0) include + * current folder (terminated with the separator), when + * the LIST pattern is '%' or '*'. i.e, + * returns "mail/" as the first LIST response. + * + * Doesn't make sense to include the current folder in this + * case, so we filter it out. Note that I'm assuming that + * the offending response is the *first* one, my experiments + * with the UW & SIMS2.0 servers indicate that .. + */ + int start = 0; + // Check the first LIST response. + if (li.length > 0 && li[0].name.equals(fullName + c)) + start = 1; // start from index = 1 + + IMAPFolder[] folders = new IMAPFolder[li.length - start]; + IMAPStore st = (IMAPStore)store; + for (int i = start; i < li.length; i++) + folders[i-start] = st.newIMAPFolder(li[i]); + return folders; + } + + /** + * Get the separator character. + */ + @Override + public synchronized char getSeparator() throws MessagingException { + if (separator == UNKNOWN_SEPARATOR) { + ListInfo[] li = null; + + li = (ListInfo[])doCommand(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + // REV1 allows the following LIST format to obtain + // the hierarchy delimiter of non-existent folders + if (p.isREV1()) // IMAP4rev1 + return p.list(fullName, ""); + else // IMAP4, note that this folder must exist for this + // to work :( + return p.list("", fullName); + } + }); + + if (li != null) + separator = li[0].separator; + else + separator = '/'; // punt ! + } + return separator; + } + + /** + * Get the type of this folder. + */ + @Override + public synchronized int getType() throws MessagingException { + if (opened) { + // never throw FolderNotFoundException if folder is open + if (attributes == null) + exists(); // try to fetch attributes + } else { + checkExists(); + } + return type; + } + + /** + * Check whether this folder is subscribed. + */ + @Override + public synchronized boolean isSubscribed() { + ListInfo[] li = null; + final String lname; + if (isNamespace && separator != '\0') + lname = fullName + separator; + else + lname = fullName; + + try { + li = (ListInfo[])doProtocolCommand(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + return p.lsub("", lname); + } + }); + } catch (ProtocolException pex) { + } + + if (li != null) { + int i = findName(li, lname); + return li[i].canOpen; + } else + return false; + } + + /** + * Subscribe/Unsubscribe this folder. + */ + @Override + public synchronized void setSubscribed(final boolean subscribe) + throws MessagingException { + doCommandIgnoreFailure(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) throws ProtocolException { + if (subscribe) + p.subscribe(fullName); + else + p.unsubscribe(fullName); + return null; + } + }); + } + + /** + * Create this folder, with the specified type. + */ + @Override + public synchronized boolean create(final int type) + throws MessagingException { + + char c = 0; + if ((type & HOLDS_MESSAGES) == 0) // only holds folders + c = getSeparator(); + final char sep = c; + Object ret = doCommandIgnoreFailure(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + if ((type & HOLDS_MESSAGES) == 0) // only holds folders + p.create(fullName + sep); + else { + p.create(fullName); + + // Certain IMAP servers do not allow creation of folders + // that can contain messages *and* subfolders. So, if we + // were asked to create such a folder, we should verify + // that we could indeed do so. + if ((type & HOLDS_FOLDERS) != 0) { + // we want to hold subfolders and messages. Check + // whether we could create such a folder. + ListInfo[] li = p.list("", fullName); + if (li != null && !li[0].hasInferiors) { + // Hmm ..the new folder + // doesn't support Inferiors ? Fail + p.delete(fullName); + throw new ProtocolException("Unsupported type"); + } + } + } + return Boolean.TRUE; + } + }); + + if (ret == null) + return false; // CREATE failure, maybe this + // folder already exists ? + + // exists = true; + // this.type = type; + boolean retb = exists(); // set exists, type, and attributes + if (retb) // Notify listeners on self and our Store + notifyFolderListeners(FolderEvent.CREATED); + return retb; + } + + /** + * Check whether this folder has new messages. + */ + @Override + public synchronized boolean hasNewMessages() throws MessagingException { + synchronized (messageCacheLock) { + if (opened) { // If we are open, we already have this information + // Folder is open, make sure information is up to date + // tickle the folder and store connections. + try { + keepConnectionAlive(true); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + return recent > 0 ? true : false; + } + } + + // First, the cheap way - use LIST and look for the \Marked + // or \Unmarked tag + + ListInfo[] li = null; + final String lname; + if (isNamespace && separator != '\0') + lname = fullName + separator; + else + lname = fullName; + li = (ListInfo[])doCommandIgnoreFailure(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) throws ProtocolException { + return p.list("", lname); + } + }); + + // if folder doesn't exist, throw exception + if (li == null) + throw new FolderNotFoundException(this, fullName + " not found"); + + int i = findName(li, lname); + if (li[i].changeState == ListInfo.CHANGED) + return true; + else if (li[i].changeState == ListInfo.UNCHANGED) + return false; + + // LIST didn't work. Try the hard way, using STATUS + try { + Status status = getStatus(); + if (status.recent > 0) + return true; + else + return false; + } catch (BadCommandException bex) { + // Probably doesn't support STATUS, tough luck. + return false; + } catch (ConnectionException cex) { + throw new StoreClosedException(store, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Get the named subfolder. + */ + @Override + public synchronized Folder getFolder(String name) + throws MessagingException { + // If we know that this folder is *not* a directory, don't + // send the request to the server at all ... + if (attributes != null && !isDirectory()) + throw new MessagingException("Cannot contain subfolders"); + + char c = getSeparator(); + return ((IMAPStore)store).newIMAPFolder(fullName + c + name, c); + } + + /** + * Delete this folder. + */ + @Override + public synchronized boolean delete(boolean recurse) + throws MessagingException { + checkClosed(); // insure that this folder is closed. + + if (recurse) { + // Delete all subfolders. + Folder[] f = list(); + for (int i = 0; i < f.length; i++) + f[i].delete(recurse); // ignore intermediate failures + } + + // Attempt to delete this folder + + Object ret = doCommandIgnoreFailure(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) throws ProtocolException { + p.delete(fullName); + return Boolean.TRUE; + } + }); + + if (ret == null) + // Non-existent folder/No permission ?? + return false; + + // DELETE succeeded. + exists = false; + attributes = null; + + // Notify listeners on self and our Store + notifyFolderListeners(FolderEvent.DELETED); + return true; + } + + /** + * Rename this folder. + */ + @Override + public synchronized boolean renameTo(final Folder f) + throws MessagingException { + checkClosed(); // insure that we are closed. + checkExists(); + if (f.getStore() != store) + throw new MessagingException("Can't rename across Stores"); + + + Object ret = doCommandIgnoreFailure(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) throws ProtocolException { + p.rename(fullName, f.getFullName()); + return Boolean.TRUE; + } + }); + + if (ret == null) + return false; + + exists = false; + attributes = null; + notifyFolderRenamedListeners(f); + return true; + } + + /** + * Open this folder in the given mode. + */ + @Override + public synchronized void open(int mode) throws MessagingException { + open(mode, null); + } + + /** + * Open this folder in the given mode, with the given + * resynchronization data. + * + * @param mode the open mode (Folder.READ_WRITE or Folder.READ_ONLY) + * @param rd the ResyncData instance + * @return a List of MailEvent instances, or null if none + * @exception MessagingException if the open fails + * @since JavaMail 1.5.1 + */ + public synchronized List open(int mode, ResyncData rd) + throws MessagingException { + checkClosed(); // insure that we are not already open + + MailboxInfo mi = null; + // Request store for our own protocol connection. + protocol = ((IMAPStore)store).getProtocol(this); + + List openEvents = null; + synchronized(messageCacheLock) { // Acquire messageCacheLock + + /* + * Add response handler right away so we get any alerts or + * notifications that occur during the SELECT or EXAMINE. + * Have to be sure to remove it if we fail to open the + * folder. + */ + protocol.addResponseHandler(this); + + try { + /* + * Enable QRESYNC or CONDSTORE if needed and not enabled. + * QRESYNC implies CONDSTORE, but servers that support + * QRESYNC are not required to support just CONDSTORE + * per RFC 5162. + */ + if (rd != null) { + if (rd == ResyncData.CONDSTORE) { + if (!protocol.isEnabled("CONDSTORE") && + !protocol.isEnabled("QRESYNC")) { + if (protocol.hasCapability("CONDSTORE")) + protocol.enable("CONDSTORE"); + else + protocol.enable("QRESYNC"); + } + } else { + if (!protocol.isEnabled("QRESYNC")) + protocol.enable("QRESYNC"); + } + } + + if (mode == READ_ONLY) + mi = protocol.examine(fullName, rd); + else + mi = protocol.select(fullName, rd); + } catch (CommandFailedException cex) { + /* + * Handle SELECT or EXAMINE failure. + * Try to figure out why the operation failed so we can + * report a more reasonable exception. + * + * Will use our existing protocol object. + */ + try { + checkExists(); // throw exception if folder doesn't exist + + if ((type & HOLDS_MESSAGES) == 0) + throw new MessagingException( + "folder cannot contain messages"); + throw new MessagingException(cex.getMessage(), cex); + + } finally { + // folder not open, don't keep this information + exists = false; + attributes = null; + type = 0; + // connection still good, return it + releaseProtocol(true); + } + // NOTREACHED + } catch (ProtocolException pex) { + // got a BAD or a BYE; connection may be bad, close it + try { + throw logoutAndThrow(pex.getMessage(), pex); + } finally { + releaseProtocol(false); + } + } + + if (mi.mode != mode) { + if (mode == READ_WRITE && mi.mode == READ_ONLY && + ((IMAPStore)store).allowReadOnlySelect()) { + ; // all ok, allow it + } else { // otherwise, it's an error + ReadOnlyFolderException ife = new ReadOnlyFolderException( + this, "Cannot open in desired mode"); + throw cleanupAndThrow(ife); + } + } + + // Initialize stuff. + opened = true; + reallyClosed = false; + this.mode = mi.mode; + availableFlags = mi.availableFlags; + permanentFlags = mi.permanentFlags; + total = realTotal = mi.total; + recent = mi.recent; + uidvalidity = mi.uidvalidity; + uidnext = mi.uidnext; + uidNotSticky = mi.uidNotSticky; + highestmodseq = mi.highestmodseq; + + // Create the message cache of appropriate size + messageCache = new MessageCache(this, (IMAPStore)store, total); + + // process saved responses and return corresponding events + if (mi.responses != null) { + openEvents = new ArrayList<>(); + for (IMAPResponse ir : mi.responses) { + if (ir.keyEquals("VANISHED")) { + // "VANISHED" SP ["(EARLIER)"] SP known-uids + String[] s = ir.readAtomStringList(); + // check that it really is "EARLIER" + if (s == null || s.length != 1 || + !s[0].equalsIgnoreCase("EARLIER")) + continue; // it's not, what to do with it here? + String uids = ir.readAtom(); + UIDSet[] uidset = UIDSet.parseUIDSets(uids); + long[] luid = UIDSet.toArray(uidset, uidnext); + if (luid != null && luid.length > 0) + openEvents.add( + new MessageVanishedEvent(this, luid)); + } else if (ir.keyEquals("FETCH")) { + assert ir instanceof FetchResponse : + "!ir instanceof FetchResponse"; + Message msg = processFetchResponse((FetchResponse)ir); + if (msg != null) + openEvents.add(new MessageChangedEvent(this, + MessageChangedEvent.FLAGS_CHANGED, msg)); + } + } + } + } // Release lock + + exists = true; // if we opened it, it must exist + attributes = null; // but we don't yet know its attributes + type = HOLDS_MESSAGES; // lacking more info, we know at least this much + + // notify listeners + notifyConnectionListeners(ConnectionEvent.OPENED); + + return openEvents; + } + + private MessagingException cleanupAndThrow(MessagingException ife) { + try { + try { + // close mailbox and return connection + protocol.close(); + releaseProtocol(true); + } catch (ProtocolException pex) { + // something went wrong, close connection + try { + addSuppressed(ife, logoutAndThrow(pex.getMessage(), pex)); + } finally { + releaseProtocol(false); + } + } + } catch (Throwable thr) { + addSuppressed(ife, thr); + } + return ife; + } + + private MessagingException logoutAndThrow(String why, ProtocolException t) { + MessagingException ife = new MessagingException(why, t); + try { + protocol.logout(); + } catch (Throwable thr) { + addSuppressed(ife, thr); + } + return ife; + } + + private void addSuppressed(Throwable ife, Throwable thr) { + if (isRecoverable(thr)) { + ife.addSuppressed(thr); + } else { + thr.addSuppressed(ife); + if (thr instanceof Error) { + throw (Error) thr; + } + if (thr instanceof RuntimeException) { + throw (RuntimeException) thr; + } + throw new RuntimeException("unexpected exception", thr); + } + } + + private boolean isRecoverable(Throwable t) { + return (t instanceof Exception) || (t instanceof LinkageError); + } + + /** + * Prefetch attributes, based on the given FetchProfile. + */ + @Override + public synchronized void fetch(Message[] msgs, FetchProfile fp) + throws MessagingException { + // cache this information in case connection is closed and + // protocol is set to null + boolean isRev1; + FetchItem[] fitems; + synchronized (messageCacheLock) { + checkOpened(); + isRev1 = protocol.isREV1(); + fitems = protocol.getFetchItems(); + } + + StringBuilder command = new StringBuilder(); + boolean first = true; + boolean allHeaders = false; + + if (fp.contains(FetchProfile.Item.ENVELOPE)) { + command.append(getEnvelopeCommand()); + first = false; + } + if (fp.contains(FetchProfile.Item.FLAGS)) { + command.append(first ? "FLAGS" : " FLAGS"); + first = false; + } + if (fp.contains(FetchProfile.Item.CONTENT_INFO)) { + command.append(first ? "BODYSTRUCTURE" : " BODYSTRUCTURE"); + first = false; + } + if (fp.contains(UIDFolder.FetchProfileItem.UID)) { + command.append(first ? "UID" : " UID"); + first = false; + } + if (fp.contains(IMAPFolder.FetchProfileItem.HEADERS)) { + allHeaders = true; + if (isRev1) + command.append(first ? + "BODY.PEEK[HEADER]" : " BODY.PEEK[HEADER]"); + else + command.append(first ? "RFC822.HEADER" : " RFC822.HEADER"); + first = false; + } + if (fp.contains(IMAPFolder.FetchProfileItem.MESSAGE)) { + allHeaders = true; + if (isRev1) + command.append(first ? "BODY.PEEK[]" : " BODY.PEEK[]"); + else + command.append(first ? "RFC822" : " RFC822"); + first = false; + } + if (fp.contains(FetchProfile.Item.SIZE) || + fp.contains(IMAPFolder.FetchProfileItem.SIZE)) { + command.append(first ? "RFC822.SIZE" : " RFC822.SIZE"); + first = false; + } + if (fp.contains(IMAPFolder.FetchProfileItem.INTERNALDATE)) { + command.append(first ? "INTERNALDATE" : " INTERNALDATE"); + first = false; + } + + // if we're not fetching all headers, fetch individual headers + String[] hdrs = null; + if (!allHeaders) { + hdrs = fp.getHeaderNames(); + if (hdrs.length > 0) { + if (!first) + command.append(" "); + command.append(createHeaderCommand(hdrs, isRev1)); + } + } + + /* + * Add any additional extension fetch items. + */ + for (int i = 0; i < fitems.length; i++) { + if (fp.contains(fitems[i].getFetchProfileItem())) { + if (command.length() != 0) + command.append(" "); + command.append(fitems[i].getName()); + } + } + + Utility.Condition condition = + new IMAPMessage.FetchProfileCondition(fp, fitems); + + // Acquire the Folder's MessageCacheLock. + synchronized (messageCacheLock) { + + // check again to make sure folder is still open + checkOpened(); + + // Apply the test, and get the sequence-number set for + // the messages that need to be prefetched. + MessageSet[] msgsets = Utility.toMessageSetSorted(msgs, condition); + + if (msgsets == null) + // We already have what we need. + return; + + Response[] r = null; + // to collect non-FETCH responses & unsolicited FETCH FLAG responses + List v = new ArrayList<>(); + try { + r = getProtocol().fetch(msgsets, command.toString()); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (CommandFailedException cfx) { + // Ignore these, as per RFC 2180 + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + + if (r == null) + return; + + for (int i = 0; i < r.length; i++) { + if (r[i] == null) + continue; + if (!(r[i] instanceof FetchResponse)) { + v.add(r[i]); // Unsolicited Non-FETCH response + continue; + } + + // Got a FetchResponse. + FetchResponse f = (FetchResponse)r[i]; + // Get the corresponding message. + IMAPMessage msg = getMessageBySeqNumber(f.getNumber()); + + int count = f.getItemCount(); + boolean unsolicitedFlags = false; + + for (int j = 0; j < count; j++) { + Item item = f.getItem(j); + // Check for the FLAGS item + if (item instanceof Flags && + (!fp.contains(FetchProfile.Item.FLAGS) || + msg == null)) { + // Ok, Unsolicited FLAGS update. + unsolicitedFlags = true; + } else if (msg != null) + msg.handleFetchItem(item, hdrs, allHeaders); + } + if (msg != null) + msg.handleExtensionFetchItems(f.getExtensionItems()); + + // If this response contains any unsolicited FLAGS + // add it to the unsolicited response vector + if (unsolicitedFlags) + v.add(f); + } + + // Dispatch any unsolicited responses + if (!v.isEmpty()) { + Response[] responses = new Response[v.size()]; + v.toArray(responses); + handleResponses(responses); + } + + } // Release messageCacheLock + } + + /** + * Return the IMAP FETCH items to request in order to load + * all the "envelope" data. Subclasses can override this + * method to fetch more data when FetchProfile.Item.ENVELOPE + * is requested. + * + * @return the IMAP FETCH items to request + * @since JavaMail 1.4.6 + */ + protected String getEnvelopeCommand() { + return IMAPMessage.EnvelopeCmd; + } + + /** + * Create a new IMAPMessage object to represent the given message number. + * Subclasses of IMAPFolder may override this method to create a + * subclass of IMAPMessage. + * + * @param msgnum the message sequence number + * @return the new IMAPMessage object + * @since JavaMail 1.4.6 + */ + protected IMAPMessage newIMAPMessage(int msgnum) { + return new IMAPMessage(this, msgnum); + } + + /** + * Create the appropriate IMAP FETCH command items to fetch the + * requested headers. + */ + private String createHeaderCommand(String[] hdrs, boolean isRev1) { + StringBuilder sb; + + if (isRev1) + sb = new StringBuilder("BODY.PEEK[HEADER.FIELDS ("); + else + sb = new StringBuilder("RFC822.HEADER.LINES ("); + + for (int i = 0; i < hdrs.length; i++) { + if (i > 0) + sb.append(" "); + sb.append(hdrs[i]); + } + + if (isRev1) + sb.append(")]"); + else + sb.append(")"); + + return sb.toString(); + } + + /** + * Set the specified flags for the given array of messages. + */ + @Override + public synchronized void setFlags(Message[] msgs, Flags flag, boolean value) + throws MessagingException { + checkOpened(); + checkFlags(flag); // validate flags + + if (msgs.length == 0) // boundary condition + return; + + synchronized(messageCacheLock) { + try { + IMAPProtocol p = getProtocol(); + MessageSet[] ms = Utility.toMessageSetSorted(msgs, null); + if (ms == null) + throw new MessageRemovedException( + "Messages have been removed"); + p.storeFlags(ms, flag, value); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } + + /** + * Set the specified flags for the given range of message numbers. + */ + @Override + public synchronized void setFlags(int start, int end, + Flags flag, boolean value) throws MessagingException { + checkOpened(); + Message[] msgs = new Message[end - start + 1]; + int i = 0; + for (int n = start; n <= end; n++) + msgs[i++] = getMessage(n); + setFlags(msgs, flag, value); + } + + /** + * Set the specified flags for the given array of message numbers. + */ + @Override + public synchronized void setFlags(int[] msgnums, Flags flag, boolean value) + throws MessagingException { + checkOpened(); + Message[] msgs = new Message[msgnums.length]; + for (int i = 0; i < msgnums.length; i++) + msgs[i] = getMessage(msgnums[i]); + setFlags(msgs, flag, value); + } + + /** + * Close this folder. + */ + @Override + public synchronized void close(boolean expunge) throws MessagingException { + close(expunge, false); + } + + /** + * Close this folder without waiting for the server. + * + * @exception MessagingException for failures + */ + public synchronized void forceClose() throws MessagingException { + close(false, true); + } + + /* + * Common close method. + */ + private void close(boolean expunge, boolean force) + throws MessagingException { + assert Thread.holdsLock(this); + synchronized(messageCacheLock) { + /* + * If we already know we're closed, this is illegal. + * Can't use checkOpened() because if we were forcibly + * closed asynchronously we just want to complete the + * closing here. + */ + if (!opened && reallyClosed) + throw new IllegalStateException( + "This operation is not allowed on a closed folder" + ); + + reallyClosed = true; // Ok, lets reset + + // Maybe this folder is already closed, or maybe another + // thread which had the messageCacheLock earlier, found + // that our server connection is dead and cleaned up + // everything .. + if (!opened) + return; + + boolean reuseProtocol = true; + try { + waitIfIdle(); + if (force) { + logger.log(Level.FINE, "forcing folder {0} to close", + fullName); + if (protocol != null) + protocol.disconnect(); + } else if (((IMAPStore)store).isConnectionPoolFull()) { + // If the connection pool is full, logout the connection + logger.fine( + "pool is full, not adding an Authenticated connection"); + + // If the expunge flag is set, close the folder first. + if (expunge && protocol != null) + protocol.close(); + + if (protocol != null) + protocol.logout(); + } else { + // If the expunge flag is set or we're open read-only we + // can just close the folder, otherwise open it read-only + // before closing, or unselect it if supported. + if (!expunge && mode == READ_WRITE) { + try { + if (protocol != null && + protocol.hasCapability("UNSELECT")) + protocol.unselect(); + else { + // Unselect isn't supported so we need to + // select a folder to cause this one to be + // deselected without expunging messages. + // We try to do that by reopening the current + // folder read-only. If the current folder + // was renamed out from under us, the EXAMINE + // might fail, but that's ok because it still + // leaves us with the folder deselected. + if (protocol != null) { + boolean selected = true; + try { + protocol.examine(fullName); + // success, folder still selected + } catch (CommandFailedException ex) { + // EXAMINE failed, folder is no + // longer selected + selected = false; + } + if (selected && protocol != null) + protocol.close(); + } + } + } catch (ProtocolException pex2) { + reuseProtocol = false; // something went wrong + } + } else { + if (protocol != null) + protocol.close(); + } + } + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + // cleanup if we haven't already + if (opened) + cleanup(reuseProtocol); + } + } + } + + // NOTE: this method can currently be invoked from close() or + // from handleResponses(). Both invocations are conditional, + // based on the "opened" flag, so we are sure that multiple + // Connection.CLOSED events are not generated. Also both + // invocations are from within messageCacheLock-ed areas. + private void cleanup(boolean returnToPool) { + assert Thread.holdsLock(messageCacheLock); + releaseProtocol(returnToPool); + messageCache = null; + uidTable = null; + exists = false; // to force a recheck in exists(). + attributes = null; + opened = false; + idleState = RUNNING; // just in case + messageCacheLock.notifyAll(); // wake up anyone waiting + notifyConnectionListeners(ConnectionEvent.CLOSED); + } + + /** + * Check whether this connection is really open. + */ + @Override + public synchronized boolean isOpen() { + synchronized(messageCacheLock) { + // Probe the connection to make sure its really open. + if (opened) { + try { + keepConnectionAlive(false); + } catch (ProtocolException pex) { } + } + } + + return opened; + } + + /** + * Return the permanent flags supported by the server. + */ + @Override + public synchronized Flags getPermanentFlags() { + if (permanentFlags == null) + return null; + return (Flags)(permanentFlags.clone()); + } + + /** + * Get the total message count. + */ + @Override + public synchronized int getMessageCount() throws MessagingException { + synchronized (messageCacheLock) { + if (opened) { + // Folder is open, we know what the total message count is .. + // tickle the folder and store connections. + try { + keepConnectionAlive(true); + return total; + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } + + // If this folder is not yet open, we use STATUS to + // get the total message count + checkExists(); + try { + Status status = getStatus(); + return status.total; + } catch (BadCommandException bex) { + // doesn't support STATUS, probably vanilla IMAP4 .. + // lets try EXAMINE + IMAPProtocol p = null; + + try { + p = getStoreProtocol(); // XXX + MailboxInfo minfo = p.examine(fullName); + p.close(); + return minfo.total; + } catch (ProtocolException pex) { + // Give up. + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + } catch (ConnectionException cex) { + throw new StoreClosedException(store, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Get the new message count. + */ + @Override + public synchronized int getNewMessageCount() throws MessagingException { + synchronized (messageCacheLock) { + if (opened) { + // Folder is open, we know what the new message count is .. + // tickle the folder and store connections. + try { + keepConnectionAlive(true); + return recent; + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } + + // If this folder is not yet open, we use STATUS to + // get the new message count + checkExists(); + try { + Status status = getStatus(); + return status.recent; + } catch (BadCommandException bex) { + // doesn't support STATUS, probably vanilla IMAP4 .. + // lets try EXAMINE + IMAPProtocol p = null; + + try { + p = getStoreProtocol(); // XXX + MailboxInfo minfo = p.examine(fullName); + p.close(); + return minfo.recent; + } catch (ProtocolException pex) { + // Give up. + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + } catch (ConnectionException cex) { + throw new StoreClosedException(store, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Get the unread message count. + */ + @Override + public synchronized int getUnreadMessageCount() + throws MessagingException { + if (!opened) { + checkExists(); + // If this folder is not yet open, we use STATUS to + // get the unseen message count + try { + Status status = getStatus(); + return status.unseen; + } catch (BadCommandException bex) { + // doesn't support STATUS, probably vanilla IMAP4 .. + // Could EXAMINE, SEARCH for UNREAD messages and + // return the count .. bah, not worth it. + return -1; + } catch (ConnectionException cex) { + throw new StoreClosedException(store, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + // if opened, issue server-side search for messages that do + // *not* have the SEEN flag. + Flags f = new Flags(); + f.add(Flags.Flag.SEEN); + try { + synchronized(messageCacheLock) { + int[] matches = getProtocol().search(new FlagTerm(f, false)); + return matches.length; // NOTE: 'matches' is never null + } + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + // Shouldn't happen + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Get the deleted message count. + */ + @Override + public synchronized int getDeletedMessageCount() + throws MessagingException { + if (!opened) { + checkExists(); + // no way to do this on closed folders + return -1; + } + + // if opened, issue server-side search for messages that do + // have the DELETED flag. + Flags f = new Flags(); + f.add(Flags.Flag.DELETED); + try { + synchronized(messageCacheLock) { + int[] matches = getProtocol().search(new FlagTerm(f, true)); + return matches.length; // NOTE: 'matches' is never null + } + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + // Shouldn't happen + throw new MessagingException(pex.getMessage(), pex); + } + } + + /* + * Get results of STATUS command for this folder, checking cache first. + * ASSERT: Must be called with this folder's synchronization lock held. + * ASSERT: The folder must be closed. + */ + private Status getStatus() throws ProtocolException { + int statusCacheTimeout = ((IMAPStore)store).getStatusCacheTimeout(); + + // if allowed to cache and our cache is still valid, return it + if (statusCacheTimeout > 0 && cachedStatus != null && + System.currentTimeMillis() - cachedStatusTime < statusCacheTimeout) + return cachedStatus; + + IMAPProtocol p = null; + + try { + p = getStoreProtocol(); // XXX + Status s = p.status(fullName, null); + // if allowed to cache, do so + if (statusCacheTimeout > 0) { + cachedStatus = s; + cachedStatusTime = System.currentTimeMillis(); + } + return s; + } finally { + releaseStoreProtocol(p); + } + } + + /** + * Get the specified message. + */ + @Override + public synchronized Message getMessage(int msgnum) + throws MessagingException { + checkOpened(); + checkRange(msgnum); + + return messageCache.getMessage(msgnum); + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized Message[] getMessages() throws MessagingException { + /* + * Need to override Folder method to throw FolderClosedException + * instead of IllegalStateException if not really closed. + */ + checkOpened(); + int total = getMessageCount(); + Message[] msgs = new Message[total]; + for (int i = 1; i <= total; i++) + msgs[i - 1] = messageCache.getMessage(i); + return msgs; + } + + /** + * Append the given messages into this folder. + */ + @Override + public synchronized void appendMessages(Message[] msgs) + throws MessagingException { + checkExists(); // verify that self exists + + // XXX - have to verify that messages are in a different + // store (if any) than target folder, otherwise could + // deadlock trying to fetch messages on the same connection + // we're using for the append. + + int maxsize = ((IMAPStore)store).getAppendBufferSize(); + + for (int i = 0; i < msgs.length; i++) { + final Message m = msgs[i]; + Date d = m.getReceivedDate(); // retain dates + if (d == null) + d = m.getSentDate(); + final Date dd = d; + final Flags f = m.getFlags(); + + final MessageLiteral mos; + try { + // if we know the message is too big, don't buffer any of it + mos = new MessageLiteral(m, + m.getSize() > maxsize ? 0 : maxsize); + } catch (IOException ex) { + throw new MessagingException( + "IOException while appending messages", ex); + } catch (MessageRemovedException mrex) { + continue; // just skip this expunged message + } + + doCommand(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + p.append(fullName, f, dd, mos); + return null; + } + }); + } + } + + /** + * Append the given messages into this folder. + * Return array of AppendUID objects containing + * UIDs of these messages in the destination folder. + * Each element of the returned array corresponds to + * an element of the msgs array. A null + * element means the server didn't return UID information + * for the appended message.

+ * + * Depends on the APPENDUID response code defined by the + * UIDPLUS extension - + * RFC 4315. + * + * @param msgs the messages to append + * @return array of AppendUID objects + * @exception MessagingException for failures + * @since JavaMail 1.4 + */ + public synchronized AppendUID[] appendUIDMessages(Message[] msgs) + throws MessagingException { + checkExists(); // verify that self exists + + // XXX - have to verify that messages are in a different + // store (if any) than target folder, otherwise could + // deadlock trying to fetch messages on the same connection + // we're using for the append. + + int maxsize = ((IMAPStore)store).getAppendBufferSize(); + + AppendUID[] uids = new AppendUID[msgs.length]; + for (int i = 0; i < msgs.length; i++) { + final Message m = msgs[i]; + final MessageLiteral mos; + + try { + // if we know the message is too big, don't buffer any of it + mos = new MessageLiteral(m, + m.getSize() > maxsize ? 0 : maxsize); + } catch (IOException ex) { + throw new MessagingException( + "IOException while appending messages", ex); + } catch (MessageRemovedException mrex) { + continue; // just skip this expunged message + } + + Date d = m.getReceivedDate(); // retain dates + if (d == null) + d = m.getSentDate(); + final Date dd = d; + final Flags f = m.getFlags(); + AppendUID auid = (AppendUID)doCommand(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + return p.appenduid(fullName, f, dd, mos); + } + }); + uids[i] = auid; + } + return uids; + } + + /** + * Append the given messages into this folder. + * Return array of Message objects representing + * the messages in the destination folder. Note + * that the folder must be open. + * Each element of the returned array corresponds to + * an element of the msgs array. A null + * element means the server didn't return UID information + * for the appended message.

+ * + * Depends on the APPENDUID response code defined by the + * UIDPLUS extension - + * RFC 4315. + * + * @param msgs the messages to add + * @return the messages in this folder + * @exception MessagingException for failures + * @since JavaMail 1.4 + */ + public synchronized Message[] addMessages(Message[] msgs) + throws MessagingException { + checkOpened(); + Message[] rmsgs = new MimeMessage[msgs.length]; + AppendUID[] uids = appendUIDMessages(msgs); + for (int i = 0; i < uids.length; i++) { + AppendUID auid = uids[i]; + if (auid != null) { + if (auid.uidvalidity == uidvalidity) { + try { + rmsgs[i] = getMessageByUID(auid.uid); + } catch (MessagingException mex) { + // ignore errors at this stage + } + } + } + } + return rmsgs; + } + + /** + * Copy the specified messages from this folder, to the + * specified destination. + */ + @Override + public synchronized void copyMessages(Message[] msgs, Folder folder) + throws MessagingException { + copymoveMessages(msgs, folder, false); + } + + /** + * Copy the specified messages from this folder, to the + * specified destination. + * Return array of AppendUID objects containing + * UIDs of these messages in the destination folder. + * Each element of the returned array corresponds to + * an element of the msgs array. A null + * element means the server didn't return UID information + * for the copied message.

+ * + * Depends on the COPYUID response code defined by the + * UIDPLUS extension - + * RFC 4315. + * + * @param msgs the messages to copy + * @param folder the folder to copy the messages to + * @return array of AppendUID objects + * @exception MessagingException for failures + * @since JavaMail 1.5.1 + */ + public synchronized AppendUID[] copyUIDMessages(Message[] msgs, + Folder folder) throws MessagingException { + return copymoveUIDMessages(msgs, folder, false); + } + + /** + * Move the specified messages from this folder, to the + * specified destination. + * + * Depends on the MOVE extension + * (RFC 6851). + * + * @param msgs the messages to move + * @param folder the folder to move the messages to + * @exception MessagingException for failures + * + * @since JavaMail 1.5.4 + */ + public synchronized void moveMessages(Message[] msgs, Folder folder) + throws MessagingException { + copymoveMessages(msgs, folder, true); + } + + /** + * Move the specified messages from this folder, to the + * specified destination. + * Return array of AppendUID objects containing + * UIDs of these messages in the destination folder. + * Each element of the returned array corresponds to + * an element of the msgs array. A null + * element means the server didn't return UID information + * for the moved message.

+ * + * Depends on the MOVE extension + * (RFC 6851) + * and the COPYUID response code defined by the + * UIDPLUS extension + * (RFC 4315). + * + * @param msgs the messages to move + * @param folder the folder to move the messages to + * @return array of AppendUID objects + * @exception MessagingException for failures + * @since JavaMail 1.5.4 + */ + public synchronized AppendUID[] moveUIDMessages(Message[] msgs, + Folder folder) throws MessagingException { + return copymoveUIDMessages(msgs, folder, true); + } + + /** + * Copy or move the specified messages from this folder, to the + * specified destination. + * + * @since JavaMail 1.5.4 + */ + private synchronized void copymoveMessages(Message[] msgs, Folder folder, + boolean move) throws MessagingException { + checkOpened(); + + if (msgs.length == 0) // boundary condition + return; + + // If the destination belongs to our same store, optimize + if (folder.getStore() == store) { + synchronized(messageCacheLock) { + try { + IMAPProtocol p = getProtocol(); + MessageSet[] ms = Utility.toMessageSet(msgs, null); + if (ms == null) + throw new MessageRemovedException( + "Messages have been removed"); + if (move) + p.move(ms, folder.getFullName()); + else + p.copy(ms, folder.getFullName()); + } catch (CommandFailedException cfx) { + if (cfx.getMessage().indexOf("TRYCREATE") != -1) + throw new FolderNotFoundException( + folder, + folder.getFullName() + " does not exist" + ); + else + throw new MessagingException(cfx.getMessage(), cfx); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } else // destination is a different store. + if (move) + throw new MessagingException( + "Move between stores not supported"); + else + super.copyMessages(msgs, folder); + } + + /** + * Copy or move the specified messages from this folder, to the + * specified destination. + * Return array of AppendUID objects containing + * UIDs of these messages in the destination folder. + * Each element of the returned array corresponds to + * an element of the msgs array. A null + * element means the server didn't return UID information + * for the copied message.

+ * + * Depends on the COPYUID response code defined by the + * UIDPLUS extension - + * RFC 4315. + * Move depends on the MOVE extension - + * RFC 6851. + * + * @param msgs the messages to copy + * @param folder the folder to copy the messages to + * @param move move instead of copy? + * @return array of AppendUID objects + * @exception MessagingException for failures + * @since JavaMail 1.5.4 + */ + private synchronized AppendUID[] copymoveUIDMessages(Message[] msgs, + Folder folder, boolean move) throws MessagingException { + checkOpened(); + + if (msgs.length == 0) // boundary condition + return null; + + // the destination must belong to our same store + if (folder.getStore() != store) // destination is a different store. + throw new MessagingException( + move ? + "can't moveUIDMessages to a different store" : + "can't copyUIDMessages to a different store"); + + // call fetch to make sure we have all the UIDs + // necessary to interpret the COPYUID response + FetchProfile fp = new FetchProfile(); + fp.add(UIDFolder.FetchProfileItem.UID); + fetch(msgs, fp); + // XXX - could pipeline the FETCH with the COPY/MOVE below + + synchronized (messageCacheLock) { + try { + IMAPProtocol p = getProtocol(); + // XXX - messages have to be from this Folder, who checks? + MessageSet[] ms = Utility.toMessageSet(msgs, null); + if (ms == null) + throw new MessageRemovedException( + "Messages have been removed"); + CopyUID cuid; + if (move) + cuid = p.moveuid(ms, folder.getFullName()); + else + cuid = p.copyuid(ms, folder.getFullName()); + + /* + * Correlate source UIDs with destination UIDs. + * This won't be time or space efficient if there's + * a lot of messages. + * + * In order to make sense of the returned UIDs, we need + * the UIDs for every one of the original messages. + * We fetch them above, to make sure we have them. + * This is critical for MOVE since after the MOVE the + * messages are gone/expunged. + * + * Assume the common case is that the messages are + * in order by UID. Map the returned source + * UIDs to their corresponding Message objects. + * Step through the msgs array looking for the + * Message object in the returned source message + * list. Most commonly the source message (UID) + * for the Nth original message will be in the Nth + * position in the returned source message (UID) + * list. Thus, the destination UID is in the Nth + * position in the returned destination UID list. + * But if the source message isn't where expected, + * we have to search the entire source message + * list, starting from where we expect it and + * wrapping around until we've searched it all. + * (Gmail will often return the lists in an unexpected order.) + * + * A possible optimization: + * If the number of UIDs returned is the same as the + * number of messages being copied/moved, we could + * sort the source messages by message number, sort + * the source and destination parallel arrays by source + * UID, and the resulting message and destination UID + * arrays will correspond. + * + * If the returned UID array size is different, some + * message was expunged while we were trying to copy/move it. + * This should be rare but would mean falling back to the + * general algorithm. + */ + long[] srcuids = UIDSet.toArray(cuid.src); + long[] dstuids = UIDSet.toArray(cuid.dst); + // map source UIDs to Message objects + // XXX - could inline/optimize this + Message[] srcmsgs = getMessagesByUID(srcuids); + AppendUID[] result = new AppendUID[msgs.length]; + for (int i = 0; i < msgs.length; i++) { + int j = i; + do { + if (msgs[i] == srcmsgs[j]) { + result[i] = new AppendUID( + cuid.uidvalidity, dstuids[j]); + break; + } + j++; + if (j >= srcmsgs.length) + j = 0; + } while (j != i); + } + return result; + } catch (CommandFailedException cfx) { + if (cfx.getMessage().indexOf("TRYCREATE") != -1) + throw new FolderNotFoundException( + folder, + folder.getFullName() + " does not exist" + ); + else + throw new MessagingException(cfx.getMessage(), cfx); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } + + /** + * Expunge all messages marked as DELETED. + */ + @Override + public synchronized Message[] expunge() throws MessagingException { + return expunge(null); + } + + /** + * Expunge the indicated messages, which must have been marked as DELETED. + * + * Depends on the UIDPLUS extension - + * RFC 4315. + * + * @param msgs the messages to expunge + * @return the expunged messages + * @exception MessagingException for failures + */ + public synchronized Message[] expunge(Message[] msgs) + throws MessagingException { + checkOpened(); + + if (msgs != null) { + // call fetch to make sure we have all the UIDs + FetchProfile fp = new FetchProfile(); + fp.add(UIDFolder.FetchProfileItem.UID); + fetch(msgs, fp); + } + + IMAPMessage[] rmsgs; + synchronized(messageCacheLock) { + doExpungeNotification = false; // We do this ourselves later + try { + IMAPProtocol p = getProtocol(); + if (msgs != null) + p.uidexpunge(Utility.toUIDSet(msgs)); + else + p.expunge(); + } catch (CommandFailedException cfx) { + // expunge not allowed, perhaps due to a permission problem? + if (mode != READ_WRITE) + throw new IllegalStateException( + "Cannot expunge READ_ONLY folder: " + fullName); + else + throw new MessagingException(cfx.getMessage(), cfx); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + // Bad bad server .. + throw new MessagingException(pex.getMessage(), pex); + } finally { + doExpungeNotification = true; + } + + // Cleanup expunged messages and sync messageCache with reality. + if (msgs != null) + rmsgs = messageCache.removeExpungedMessages(msgs); + else + rmsgs = messageCache.removeExpungedMessages(); + if (uidTable != null) { + for (int i = 0; i < rmsgs.length; i++) { + IMAPMessage m = rmsgs[i]; + /* remove this message from the UIDTable */ + long uid = m.getUID(); + if (uid != -1) + uidTable.remove(Long.valueOf(uid)); + } + } + + // Update 'total' + total = messageCache.size(); + } + + // Notify listeners. This time its for real, guys. + if (rmsgs.length > 0) + notifyMessageRemovedListeners(true, rmsgs); + return rmsgs; + } + + /** + * Search whole folder for messages matching the given term. + * If the property mail.imap.throwsearchexception is true, + * and the search term is too complex for the IMAP protocol, + * SearchException is thrown. Otherwise, if the search term is too + * complex, super.search is called to do the search on + * the client. + * + * @param term the search term + * @return the messages that match + * @exception SearchException if mail.imap.throwsearchexception is + * true and the search is too complex for the IMAP protocol + * @exception MessagingException for other failures + */ + @Override + public synchronized Message[] search(SearchTerm term) + throws MessagingException { + checkOpened(); + + try { + Message[] matchMsgs = null; + + synchronized(messageCacheLock) { + int[] matches = getProtocol().search(term); + if (matches != null) + matchMsgs = getMessagesBySeqNumbers(matches); + } + return matchMsgs; + + } catch (CommandFailedException cfx) { + // unsupported charset or search criterion + return super.search(term); + } catch (SearchException sex) { + // too complex for IMAP + if (((IMAPStore)store).throwSearchException()) + throw sex; + return super.search(term); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + // bug in our IMAP layer ? + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Search the folder for messages matching the given term. Returns + * array of matching messages. Returns an empty array if no matching + * messages are found. + */ + @Override + public synchronized Message[] search(SearchTerm term, Message[] msgs) + throws MessagingException { + checkOpened(); + + if (msgs.length == 0) + // need to return an empty array (not null!) + return msgs; + + try { + Message[] matchMsgs = null; + + synchronized(messageCacheLock) { + IMAPProtocol p = getProtocol(); + MessageSet[] ms = Utility.toMessageSetSorted(msgs, null); + if (ms == null) + throw new MessageRemovedException( + "Messages have been removed"); + int[] matches = p.search(ms, term); + if (matches != null) + matchMsgs = getMessagesBySeqNumbers(matches); + } + return matchMsgs; + + } catch (CommandFailedException cfx) { + // unsupported charset or search criterion + return super.search(term, msgs); + } catch (SearchException sex) { + // too complex for IMAP + return super.search(term, msgs); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + // bug in our IMAP layer ? + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Sort the messages in the folder according to the sort criteria. + * The messages are returned in the sorted order, but the order of + * the messages in the folder is not changed.

+ * + * Depends on the SORT extension - + * RFC 5256. + * + * @param term the SortTerms + * @return the messages in sorted order + * @exception MessagingException for failures + * @since JavaMail 1.4.4 + */ + public synchronized Message[] getSortedMessages(SortTerm[] term) + throws MessagingException { + return getSortedMessages(term, null); + } + + /** + * Sort the messages in the folder according to the sort criteria. + * The messages are returned in the sorted order, but the order of + * the messages in the folder is not changed. Only messages matching + * the search criteria are considered.

+ * + * Depends on the SORT extension - + * RFC 5256. + * + * @param term the SortTerms + * @param sterm the SearchTerm + * @return the messages in sorted order + * @exception MessagingException for failures + * @since JavaMail 1.4.4 + */ + public synchronized Message[] getSortedMessages(SortTerm[] term, + SearchTerm sterm) throws MessagingException { + checkOpened(); + + try { + Message[] matchMsgs = null; + + synchronized(messageCacheLock) { + int[] matches = getProtocol().sort(term, sterm); + if (matches != null) + matchMsgs = getMessagesBySeqNumbers(matches); + } + return matchMsgs; + + } catch (CommandFailedException cfx) { + // unsupported charset or search criterion + throw new MessagingException(cfx.getMessage(), cfx); + } catch (SearchException sex) { + // too complex for IMAP + throw new MessagingException(sex.getMessage(), sex); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + // bug in our IMAP layer ? + throw new MessagingException(pex.getMessage(), pex); + } + } + + /* + * Override Folder method to keep track of whether we have any + * message count listeners. Normally we won't have any, so we + * can avoid creating message objects to pass to the notify + * method. It's too hard to keep track of when all listeners + * are removed, and that's a rare case, so we don't try. + */ + @Override + public synchronized void addMessageCountListener(MessageCountListener l) { + super.addMessageCountListener(l); + hasMessageCountListener = true; + } + + /*********************************************************** + * UIDFolder interface methods + **********************************************************/ + + /** + * Returns the UIDValidity for this folder. + */ + @Override + public synchronized long getUIDValidity() throws MessagingException { + if (opened) // we already have this information + return uidvalidity; + + IMAPProtocol p = null; + Status status = null; + + try { + p = getStoreProtocol(); // XXX + String[] item = { "UIDVALIDITY" }; + status = p.status(fullName, item); + } catch (BadCommandException bex) { + // Probably a RFC1730 server + throw new MessagingException("Cannot obtain UIDValidity", bex); + } catch (ConnectionException cex) { + // Oops, the store or folder died on us. + throwClosedException(cex); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + + if (status == null) + throw new MessagingException("Cannot obtain UIDValidity"); + return status.uidvalidity; + } + + /** + * Returns the predicted UID that will be assigned to the + * next message that is appended to this folder. + * If the folder is closed, the STATUS command is used to + * retrieve this value. If the folder is open, the value + * returned from the SELECT or EXAMINE command is returned. + * Note that messages may have been appended to the folder + * while it was open and thus this value may be out of + * date.

+ * + * Servers implementing RFC2060 likely won't return this value + * when a folder is opened. Servers implementing RFC3501 + * should return this value when a folder is opened.

+ * + * @return the UIDNEXT value, or -1 if unknown + * @exception MessagingException for failures + * @since JavaMail 1.3.3 + */ + @Override + public synchronized long getUIDNext() throws MessagingException { + if (opened) // we already have this information + return uidnext; + + IMAPProtocol p = null; + Status status = null; + + try { + p = getStoreProtocol(); // XXX + String[] item = { "UIDNEXT" }; + status = p.status(fullName, item); + } catch (BadCommandException bex) { + // Probably a RFC1730 server + throw new MessagingException("Cannot obtain UIDNext", bex); + } catch (ConnectionException cex) { + // Oops, the store or folder died on us. + throwClosedException(cex); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + + if (status == null) + throw new MessagingException("Cannot obtain UIDNext"); + return status.uidnext; + } + + /** + * Get the Message corresponding to the given UID. + * If no such message exists, null is returned. + */ + @Override + public synchronized Message getMessageByUID(long uid) + throws MessagingException { + checkOpened(); // insure folder is open + + IMAPMessage m = null; + + try { + synchronized(messageCacheLock) { + Long l = Long.valueOf(uid); + + if (uidTable != null) { + // Check in uidTable + m = uidTable.get(l); + if (m != null) // found it + return m; + } else + uidTable = new Hashtable<>(); + + // Check with the server + // Issue UID FETCH command + getProtocol().fetchSequenceNumber(uid); + + if (uidTable != null) { + // Check in uidTable + m = uidTable.get(l); + if (m != null) // found it + return m; + } + } + } catch(ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + + return m; + } + + /** + * Get the Messages specified by the given range.

+ * Returns Message objects for all valid messages in this range. + * Returns an empty array if no messages are found. + */ + @Override + public synchronized Message[] getMessagesByUID(long start, long end) + throws MessagingException { + checkOpened(); // insure that folder is open + + Message[] msgs; // array of messages to be returned + + try { + synchronized(messageCacheLock) { + if (uidTable == null) + uidTable = new Hashtable<>(); + + // Issue UID FETCH for given range + long[] ua = getProtocol().fetchSequenceNumbers(start, end); + + List ma = new ArrayList<>(); + // NOTE: Below must be within messageCacheLock region + for (int i = 0; i < ua.length; i++) { + Message m = uidTable.get(Long.valueOf(ua[i])); + if (m != null) // found it + ma.add(m); + } + msgs = ma.toArray(new Message[ma.size()]); + } + } catch(ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + + return msgs; + } + + /** + * Get the Messages specified by the given array.

+ * + * uids.length() elements are returned. + * If any UID in the array is invalid, a null entry + * is returned for that element. + */ + @Override + public synchronized Message[] getMessagesByUID(long[] uids) + throws MessagingException { + checkOpened(); // insure that folder is open + + try { + synchronized(messageCacheLock) { + long[] unavailUids = uids; + if (uidTable != null) { + // to collect unavailable UIDs + List v = new ArrayList<>(); + for (long uid : uids) { + if (!uidTable.containsKey(uid)) { + // This UID has not been loaded yet. + v.add(uid); + } + } + + int vsize = v.size(); + unavailUids = new long[vsize]; + for (int i = 0; i < vsize; i++) { + unavailUids[i] = v.get(i); + } + } else + uidTable = new Hashtable<>(); + + if (unavailUids.length > 0) { + // Issue UID FETCH request for given uids + getProtocol().fetchSequenceNumbers(unavailUids); + } + + // Return array of size = uids.length + Message[] msgs = new Message[uids.length]; + for (int i = 0; i < uids.length; i++) + msgs[i] = (Message)uidTable.get(Long.valueOf(uids[i])); + return msgs; + } + } catch(ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Get the UID for the specified message. + */ + @Override + public synchronized long getUID(Message message) + throws MessagingException { + if (message.getFolder() != this) + throw new NoSuchElementException( + "Message does not belong to this folder"); + + checkOpened(); // insure that folder is open + + if (!(message instanceof IMAPMessage)) + throw new MessagingException("message is not an IMAPMessage"); + IMAPMessage m = (IMAPMessage)message; + // If the message already knows its UID, great .. + long uid; + if ((uid = m.getUID()) != -1) + return uid; + + synchronized(messageCacheLock) { // Acquire Lock + try { + IMAPProtocol p = getProtocol(); + m.checkExpunged(); // insure that message is not expunged + UID u = p.fetchUID(m.getSequenceNumber()); + + if (u != null) { + uid = u.uid; + m.setUID(uid); // set message's UID + + // insert this message into uidTable + if (uidTable == null) + uidTable = new Hashtable<>(); + uidTable.put(Long.valueOf(uid), m); + } + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + return uid; + } + + /** + * Servers that support the UIDPLUS extension + * (RFC 4315) + * may indicate that this folder does not support persistent UIDs; + * that is, UIDVALIDITY will be different each time the folder is + * opened. Only valid when the folder is open. + * + * @return true if UIDs are not sticky + * @exception MessagingException for failures + * @exception IllegalStateException if the folder isn't open + * @see "RFC 4315" + * @since JavaMail 1.6.0 + */ + public synchronized boolean getUIDNotSticky() throws MessagingException { + checkOpened(); + return uidNotSticky; + } + + /** + * Get or create Message objects for the UIDs. + */ + private Message[] createMessagesForUIDs(long[] uids) { + IMAPMessage[] msgs = new IMAPMessage[uids.length]; + for (int i = 0; i < uids.length; i++) { + IMAPMessage m = null; + if (uidTable != null) + m = uidTable.get(Long.valueOf(uids[i])); + if (m == null) { + // fake it, we don't know what message this really is + m = newIMAPMessage(-1); // no sequence number + m.setUID(uids[i]); + m.setExpunged(true); + } + msgs[i++] = m; + } + return msgs; + } + + /** + * Returns the HIGHESTMODSEQ for this folder. + * + * @return the HIGHESTMODSEQ value + * @exception MessagingException for failures + * @see "RFC 4551" + * @since JavaMail 1.5.1 + */ + public synchronized long getHighestModSeq() throws MessagingException { + if (opened) // we already have this information + return highestmodseq; + + IMAPProtocol p = null; + Status status = null; + + try { + p = getStoreProtocol(); // XXX + if (!p.hasCapability("CONDSTORE")) + throw new BadCommandException("CONDSTORE not supported"); + String[] item = { "HIGHESTMODSEQ" }; + status = p.status(fullName, item); + } catch (BadCommandException bex) { + // Probably a RFC1730 server + throw new MessagingException("Cannot obtain HIGHESTMODSEQ", bex); + } catch (ConnectionException cex) { + // Oops, the store or folder died on us. + throwClosedException(cex); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + + if (status == null) + throw new MessagingException("Cannot obtain HIGHESTMODSEQ"); + return status.highestmodseq; + } + + /** + * Get the messages that have been changed since the given MODSEQ value. + * Also, prefetch the flags for the messages.

+ * + * The server must support the CONDSTORE extension. + * + * @param start the first message number + * @param end the last message number + * @param modseq the MODSEQ value + * @return the changed messages + * @exception MessagingException for failures + * @see "RFC 4551" + * @since JavaMail 1.5.1 + */ + public synchronized Message[] getMessagesByUIDChangedSince( + long start, long end, long modseq) + throws MessagingException { + checkOpened(); // insure that folder is open + + try { + synchronized (messageCacheLock) { + IMAPProtocol p = getProtocol(); + if (!p.hasCapability("CONDSTORE")) + throw new BadCommandException("CONDSTORE not supported"); + + // Issue FETCH for given range + int[] nums = p.uidfetchChangedSince(start, end, modseq); + return getMessagesBySeqNumbers(nums); + } + } catch(ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Get the quotas for the quotaroot associated with this + * folder. Note that many folders may have the same quotaroot. + * Quotas are controlled on the basis of a quotaroot, not + * (necessarily) a folder. The relationship between folders + * and quotaroots depends on the IMAP server. Some servers + * might implement a single quotaroot for all folders owned by + * a user. Other servers might implement a separate quotaroot + * for each folder. A single folder can even have multiple + * quotaroots, perhaps controlling quotas for different + * resources. + * + * @return array of Quota objects for the quotaroots associated with + * this folder + * @exception MessagingException if the server doesn't support the + * QUOTA extension + */ + public Quota[] getQuota() throws MessagingException { + return (Quota[])doOptionalCommand("QUOTA not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + return p.getQuotaRoot(fullName); + } + }); + } + + /** + * Set the quotas for the quotaroot specified in the quota argument. + * Typically this will be one of the quotaroots associated with this + * folder, as obtained from the getQuota method, but it + * need not be. + * + * @param quota the quota to set + * @exception MessagingException if the server doesn't support the + * QUOTA extension + */ + public void setQuota(final Quota quota) throws MessagingException { + doOptionalCommand("QUOTA not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + p.setQuota(quota); + return null; + } + }); + } + + /** + * Get the access control list entries for this folder. + * + * @return array of access control list entries + * @exception MessagingException if the server doesn't support the + * ACL extension + */ + public ACL[] getACL() throws MessagingException { + return (ACL[])doOptionalCommand("ACL not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + return p.getACL(fullName); + } + }); + } + + /** + * Add an access control list entry to the access control list + * for this folder. + * + * @param acl the access control list entry to add + * @exception MessagingException if the server doesn't support the + * ACL extension + */ + public void addACL(ACL acl) throws MessagingException { + setACL(acl, '\0'); + } + + /** + * Remove any access control list entry for the given identifier + * from the access control list for this folder. + * + * @param name the identifier for which to remove all ACL entries + * @exception MessagingException if the server doesn't support the + * ACL extension + */ + public void removeACL(final String name) throws MessagingException { + doOptionalCommand("ACL not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + p.deleteACL(fullName, name); + return null; + } + }); + } + + /** + * Add the rights specified in the ACL to the entry for the + * identifier specified in the ACL. If an entry for the identifier + * doesn't already exist, add one. + * + * @param acl the identifer and rights to add + * @exception MessagingException if the server doesn't support the + * ACL extension + */ + public void addRights(ACL acl) throws MessagingException { + setACL(acl, '+'); + } + + /** + * Remove the rights specified in the ACL from the entry for the + * identifier specified in the ACL. + * + * @param acl the identifer and rights to remove + * @exception MessagingException if the server doesn't support the + * ACL extension + */ + public void removeRights(ACL acl) throws MessagingException { + setACL(acl, '-'); + } + + /** + * Get all the rights that may be allowed to the given identifier. + * Rights are grouped per RFC 2086 and each group is returned as an + * element of the array. The first element of the array is the set + * of rights that are always granted to the identifier. Later + * elements are rights that may be optionally granted to the + * identifier.

+ * + * Note that this method lists the rights that it is possible to + * assign to the given identifier, not the rights that are + * actually granted to the given identifier. For the latter, see + * the getACL method. + * + * @param name the identifier to list rights for + * @return array of Rights objects representing possible + * rights for the identifier + * @exception MessagingException if the server doesn't support the + * ACL extension + */ + public Rights[] listRights(final String name) throws MessagingException { + return (Rights[])doOptionalCommand("ACL not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + return p.listRights(fullName, name); + } + }); + } + + /** + * Get the rights allowed to the currently authenticated user. + * + * @return the rights granted to the current user + * @exception MessagingException if the server doesn't support the + * ACL extension + */ + public Rights myRights() throws MessagingException { + return (Rights)doOptionalCommand("ACL not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + return p.myRights(fullName); + } + }); + } + + private void setACL(final ACL acl, final char mod) + throws MessagingException { + doOptionalCommand("ACL not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + p.setACL(fullName, mod, acl); + return null; + } + }); + } + + /** + * Get the attributes that the IMAP server returns with the + * LIST response. + * + * @return array of attributes for this folder + * @exception MessagingException for failures + * @since JavaMail 1.3.3 + */ + public synchronized String[] getAttributes() throws MessagingException { + checkExists(); + if (attributes == null) + exists(); // do a LIST to set the attributes + return attributes == null ? new String[0] : attributes.clone(); + } + + /** + * Use the IMAP IDLE command (see + * RFC 2177), + * if supported by the server, to enter idle mode so that the server + * can send unsolicited notifications of new messages arriving, etc. + * without the need for the client to constantly poll the server. + * Use an appropriate listener to be notified of new messages or + * other events. When another thread (e.g., the listener thread) + * needs to issue an IMAP comand for this folder, the idle mode will + * be terminated and this method will return. Typically the caller + * will invoke this method in a loop.

+ * + * The mail.imap.minidletime property enforces a minimum delay + * before returning from this method, to ensure that other threads + * have a chance to issue commands before the caller invokes this + * method again. The default delay is 10 milliseconds. + * + * @exception MessagingException if the server doesn't support the + * IDLE extension + * @exception IllegalStateException if the folder isn't open + * + * @since JavaMail 1.4.1 + */ + public void idle() throws MessagingException { + idle(false); + } + + /** + * Like {@link #idle}, but if once is true, abort the + * IDLE command after the first notification, to allow the caller + * to process any notification synchronously. + * + * @param once only do one notification? + * @exception MessagingException if the server doesn't support the + * IDLE extension + * @exception IllegalStateException if the folder isn't open + * + * @since JavaMail 1.4.3 + */ + public void idle(boolean once) throws MessagingException { + synchronized (this) { + /* + * We can't support the idle method if we're using SocketChannels + * because SocketChannels don't allow simultaneous read and write. + * If we're blocked in a read waiting for IDLE responses, we can't + * send the DONE message to abort the IDLE. Sigh. + * XXX - We could do select here too, like IdleManager, instead + * of blocking in read, but that's more complicated. + */ + if (protocol != null && protocol.getChannel() != null) + throw new MessagingException( + "idle method not supported with SocketChannels"); + } + if (!startIdle(null)) + return; + + /* + * We gave up the folder lock so that other threads + * can get into the folder far enough to see that we're + * in IDLE and abort the IDLE. + * + * Now we read responses from the IDLE command, especially + * including unsolicited notifications from the server. + * We don't hold the messageCacheLock while reading because + * it protects the idleState and other threads need to be + * able to examine the state. + * + * The messageCacheLock is held in handleIdle while processing + * the responses so that we can update the number of messages + * in the folder (for example). + */ + for (;;) { + if (!handleIdle(once)) + break; + } + + /* + * Enforce a minimum delay to give time to threads + * processing the responses that came in while we + * were idle. + */ + int minidle = ((IMAPStore)store).getMinIdleTime(); + if (minidle > 0) { + try { + Thread.sleep(minidle); + } catch (InterruptedException ex) { + // restore the interrupted state, which callers might depend on + Thread.currentThread().interrupt(); + } + } + } + + /** + * Start the IDLE command and put this folder into the IDLE state. + * IDLE processing is done later in handleIdle(), e.g., called from + * the IdleManager. + * + * @return true if IDLE started, false otherwise + * @exception MessagingException if the server doesn't support the + * IDLE extension + * @exception IllegalStateException if the folder isn't open + * @since JavaMail 1.5.2 + */ + boolean startIdle(final IdleManager im) throws MessagingException { + // ASSERT: Must NOT be called with this folder's + // synchronization lock held. + assert !Thread.holdsLock(this); + synchronized(this) { + checkOpened(); + if (im != null && idleManager != null && im != idleManager) + throw new MessagingException( + "Folder already being watched by another IdleManager"); + Boolean started = (Boolean)doOptionalCommand("IDLE not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + // if the IdleManager is already watching this folder, + // there's nothing to do here + if (idleState == IDLE && + im != null && im == idleManager) + return Boolean.TRUE; // already watching it + if (idleState == RUNNING) { + p.idleStart(); + logger.finest("startIdle: set to IDLE"); + idleState = IDLE; + idleManager = im; + return Boolean.TRUE; + } else { + // some other thread must be running the IDLE + // command, we'll just wait for it to finish + // without aborting it ourselves + try { + // give up lock and wait to be not idle + messageCacheLock.wait(); + } catch (InterruptedException ex) { + // restore the interrupted state, which callers + // might depend on + Thread.currentThread().interrupt(); + } + return Boolean.FALSE; + } + } + }); + logger.log(Level.FINEST, "startIdle: return {0}", started); + return started.booleanValue(); + } + } + + /** + * Read a response from the server while we're in the IDLE state. + * We hold the messageCacheLock while processing the + * responses so that we can update the number of messages + * in the folder (for example). + * + * @param once only do one notification? + * @return true if we should look for more IDLE responses, + * false if IDLE is done + * @exception MessagingException for errors + * @since JavaMail 1.5.2 + */ + boolean handleIdle(boolean once) throws MessagingException { + Response r = null; + do { + r = protocol.readIdleResponse(); + try { + synchronized (messageCacheLock) { + if (r.isBYE() && r.isSynthetic() && idleState == IDLE) { + /* + * If it was a timeout and no bytes were transferred + * we ignore it and go back and read again. + * If the I/O was otherwise interrupted, and no + * bytes were transferred, we take it as a request + * to abort the IDLE. + */ + Exception ex = r.getException(); + if (ex instanceof InterruptedIOException && + ((InterruptedIOException)ex). + bytesTransferred == 0) { + if (ex instanceof SocketTimeoutException) { + logger.finest( + "handleIdle: ignoring socket timeout"); + r = null; // repeat do/while loop + } else { + logger.finest("handleIdle: interrupting IDLE"); + IdleManager im = idleManager; + if (im != null) { + logger.finest( + "handleIdle: request IdleManager to abort"); + im.requestAbort(this); + } else { + logger.finest("handleIdle: abort IDLE"); + protocol.idleAbort(); + idleState = ABORTING; + } + // normally will exit the do/while loop + } + continue; + } + } + boolean done = true; + try { + if (protocol == null || + !protocol.processIdleResponse(r)) + return false; // done + done = false; + } finally { + if (done) { + logger.finest("handleIdle: set to RUNNING"); + idleState = RUNNING; + idleManager = null; + messageCacheLock.notifyAll(); + } + } + if (once) { + if (idleState == IDLE) { + try { + protocol.idleAbort(); + } catch (Exception ex) { + // ignore any failures, still have to abort. + // connection failures will be detected above + // in the call to readIdleResponse. + } + idleState = ABORTING; + } + } + } + } catch (ConnectionException cex) { + // Oops, the folder died on us. + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + // keep processing responses already in our buffer + } while (r == null || protocol.hasResponse()); + return true; + } + + /* + * If an IDLE command is in progress, abort it if necessary, + * and wait until it completes. + * ASSERT: Must be called with the message cache lock held. + */ + void waitIfIdle() throws ProtocolException { + assert Thread.holdsLock(messageCacheLock); + while (idleState != RUNNING) { + if (idleState == IDLE) { + IdleManager im = idleManager; + if (im != null) { + logger.finest("waitIfIdle: request IdleManager to abort"); + im.requestAbort(this); + } else { + logger.finest("waitIfIdle: abort IDLE"); + protocol.idleAbort(); + idleState = ABORTING; + } + } else + logger.log(Level.FINEST, "waitIfIdle: idleState {0}", idleState); + try { + // give up lock and wait to be not idle + if (logger.isLoggable(Level.FINEST)) + logger.finest("waitIfIdle: wait to be not idle: " + + Thread.currentThread()); + messageCacheLock.wait(); + if (logger.isLoggable(Level.FINEST)) + logger.finest("waitIfIdle: wait done, idleState " + + idleState + ": " + Thread.currentThread()); + } catch (InterruptedException ex) { + // restore the interrupted state, which callers might depend on + Thread.currentThread().interrupt(); + // If someone is trying to interrupt us we can't keep going + // around the loop waiting for IDLE to complete, but we can't + // just return because callers expect the idleState to be + // RUNNING when we return. Throwing this exception seems + // like the best choice. + throw new ProtocolException("Interrupted waitIfIdle", ex); + } + } + } + + /* + * Send the DONE command that aborts the IDLE; used by IdleManager. + */ + void idleAbort() { + synchronized (messageCacheLock) { + if (idleState == IDLE && protocol != null) { + protocol.idleAbort(); + idleState = ABORTING; + } + } + } + + /* + * Send the DONE command that aborts the IDLE and wait for the response; + * used by IdleManager. + */ + void idleAbortWait() { + synchronized (messageCacheLock) { + if (idleState == IDLE && protocol != null) { + protocol.idleAbort(); + idleState = ABORTING; + + // read responses until OK or connection failure + try { + for (;;) { + if (!handleIdle(false)) + break; + } + } catch (Exception ex) { + // assume it's a connection failure; nothing more to do + logger.log(Level.FINEST, "Exception in idleAbortWait", ex); + } + logger.finest("IDLE aborted"); + } + } + } + + /** + * Return the SocketChannel for this connection, if any, for use + * in IdleManager. + */ + SocketChannel getChannel() { + return protocol != null ? protocol.getChannel() : null; + } + + /** + * Send the IMAP ID command (if supported by the server) and return + * the result from the server. The ID command identfies the client + * to the server and returns information about the server to the client. + * See RFC 2971. + * The returned Map is unmodifiable. + * + * @param clientParams a Map of keys and values identifying the client + * @return a Map of keys and values identifying the server + * @exception MessagingException if the server doesn't support the + * ID extension + * @since JavaMail 1.5.1 + */ + @SuppressWarnings("unchecked") + public Map id(final Map clientParams) + throws MessagingException { + checkOpened(); + return (Map)doOptionalCommand("ID not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + return p.id(clientParams); + } + }); + } + + /** + * Use the IMAP STATUS command to get the indicated item. + * The STATUS item may be a standard item such as "RECENT" or "UNSEEN", + * or may be a server-specific item. + * The folder must be closed. If the item is not found, or the + * folder is open, -1 is returned. + * + * @param item the STATUS item to fetch + * @return the value of the STATUS item, or -1 + * @exception MessagingException for errors + * @since JavaMail 1.5.2 + */ + public synchronized long getStatusItem(String item) + throws MessagingException { + if (!opened) { + checkExists(); + + IMAPProtocol p = null; + Status status = null; + try { + p = getStoreProtocol(); // XXX + String[] items = { item }; + status = p.status(fullName, items); + return status != null ? status.getItem(item) : -1; + } catch (BadCommandException bex) { + // doesn't support STATUS, probably vanilla IMAP4 .. + // Could EXAMINE, SEARCH for UNREAD messages and + // return the count .. bah, not worth it. + return -1; + } catch (ConnectionException cex) { + throw new StoreClosedException(store, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + } + return -1; + } + + /** + * The response handler. This is the callback routine that is + * invoked by the protocol layer. + */ + /* + * ASSERT: This method must be called only when holding the + * messageCacheLock. + * ASSERT: This method must *not* invoke any other method that + * might grab the 'folder' lock or 'message' lock (i.e., any + * synchronized methods on IMAPFolder or IMAPMessage) + * since that will result in violating the locking hierarchy. + */ + @Override + public void handleResponse(Response r) { + assert Thread.holdsLock(messageCacheLock); + + /* + * First, delegate possible ALERT or notification to the Store. + */ + if (r.isOK() || r.isNO() || r.isBAD() || r.isBYE()) + ((IMAPStore)store).handleResponseCode(r); + + /* + * Now check whether this is a BYE or OK response and + * handle appropriately. + */ + if (r.isBYE()) { + if (opened) // XXX - accessed without holding folder lock + cleanup(false); + return; + } else if (r.isOK()) { + // HIGHESTMODSEQ can be updated on any OK response + r.skipSpaces(); + if (r.readByte() == '[') { + String s = r.readAtom(); + if (s.equalsIgnoreCase("HIGHESTMODSEQ")) + highestmodseq = r.readLong(); + } + r.reset(); + return; + } else if (!r.isUnTagged()) { + return; // might be a continuation for IDLE + } + + /* Now check whether this is an IMAP specific response */ + if (!(r instanceof IMAPResponse)) { + // Probably a bug in our code ! + // XXX - should be an assert + logger.fine("UNEXPECTED RESPONSE : " + r.toString()); + return; + } + + IMAPResponse ir = (IMAPResponse)r; + + if (ir.keyEquals("EXISTS")) { // EXISTS + int exists = ir.getNumber(); + if (exists <= realTotal) + // Could be the EXISTS following EXPUNGE, ignore 'em + return; + + int count = exists - realTotal; // number of new messages + Message[] msgs = new Message[count]; + + // Add 'count' new IMAPMessage objects into the messageCache + messageCache.addMessages(count, realTotal + 1); + int oldtotal = total; // used in loop below + realTotal += count; + total += count; + + // avoid instantiating Message objects if no listeners. + if (hasMessageCountListener) { + for (int i = 0; i < count; i++) + msgs[i] = messageCache.getMessage(++oldtotal); + + // Notify listeners. + notifyMessageAddedListeners(msgs); + } + + } else if (ir.keyEquals("EXPUNGE")) { + // EXPUNGE response. + + int seqnum = ir.getNumber(); + if (seqnum > realTotal) { + // A message was expunged that we never knew about. + // Exchange will do this. Just ignore the notification. + // (Alternatively, we could simulate an EXISTS for the + // expunged message before expunging it.) + return; + } + Message[] msgs = null; + if (doExpungeNotification && hasMessageCountListener) { + // save the Message object first; can't look it + // up after it's expunged + msgs = new Message[] { getMessageBySeqNumber(seqnum) }; + if (msgs[0] == null) // XXX - should never happen + msgs = null; + } + + messageCache.expungeMessage(seqnum); + + // decrement 'realTotal'; but leave 'total' unchanged + realTotal--; + + if (msgs != null) // Do the notification here. + notifyMessageRemovedListeners(false, msgs); + + } else if (ir.keyEquals("VANISHED")) { + // after the folder is opened with QRESYNC, a VANISHED response + // without the (EARLIER) tag is used instead of the EXPUNGE + // response + + // "VANISHED" SP ["(EARLIER)"] SP known-uids + String[] s = ir.readAtomStringList(); + if (s == null) { // no (EARLIER) + String uids = ir.readAtom(); + UIDSet[] uidset = UIDSet.parseUIDSets(uids); + // assume no duplicates and no UIDs out of range + realTotal -= UIDSet.size(uidset); + long[] luid = UIDSet.toArray(uidset); + Message[] msgs = createMessagesForUIDs(luid); + for (Message m : msgs) { + if (m.getMessageNumber() > 0) + messageCache.expungeMessage(m.getMessageNumber()); + } + if (doExpungeNotification && hasMessageCountListener) { + notifyMessageRemovedListeners(true, msgs); + } + } // else if (EARLIER), ignore + + } else if (ir.keyEquals("FETCH")) { + assert ir instanceof FetchResponse : "!ir instanceof FetchResponse"; + Message msg = processFetchResponse((FetchResponse)ir); + if (msg != null) + notifyMessageChangedListeners( + MessageChangedEvent.FLAGS_CHANGED, msg); + + } else if (ir.keyEquals("RECENT")) { + // update 'recent' + recent = ir.getNumber(); + } + } + + /** + * Process a FETCH response. + * The only unsolicited FETCH response that makes sense + * to me (for now) is FLAGS updates, which might include + * UID and MODSEQ information. Ignore any other junk. + */ + private Message processFetchResponse(FetchResponse fr) { + IMAPMessage msg = getMessageBySeqNumber(fr.getNumber()); + if (msg != null) { // should always be true + boolean notify = false; + + UID uid = fr.getItem(UID.class); + if (uid != null && msg.getUID() != uid.uid) { + msg.setUID(uid.uid); + if (uidTable == null) + uidTable = new Hashtable<>(); + uidTable.put(Long.valueOf(uid.uid), msg); + notify = true; + } + + MODSEQ modseq = fr.getItem(MODSEQ.class); + if (modseq != null && msg._getModSeq() != modseq.modseq) { + msg.setModSeq(modseq.modseq); + /* + * XXX - should we update the folder's HIGHESTMODSEQ or not? + * + if (modseq.modseq > highestmodseq) + highestmodseq = modseq.modseq; + */ + notify = true; + } + + // Get FLAGS response, if present + FLAGS flags = fr.getItem(FLAGS.class); + if (flags != null) { + msg._setFlags(flags); // assume flags changed + notify = true; + } + + // handle any extension items that might've changed + // XXX - no notifications associated with extension items + msg.handleExtensionFetchItems(fr.getExtensionItems()); + + if (!notify) + msg = null; + } + return msg; + } + + /** + * Handle the given array of Responses. + * + * ASSERT: This method must be called only when holding the + * messageCacheLock + */ + void handleResponses(Response[] r) { + for (int i = 0; i < r.length; i++) { + if (r[i] != null) + handleResponse(r[i]); + } + } + + /** + * Get this folder's Store's protocol connection. + * + * When acquiring a store protocol object, it is important to + * use the following steps: + * + *

+     *     IMAPProtocol p = null;
+     *     try {
+     *         p = getStoreProtocol();
+     *         // perform the command
+     *     } catch (WhateverException ex) {
+     *         // handle it
+     *     } finally {
+     *         releaseStoreProtocol(p);
+     *     }
+     * 
+ * + * ASSERT: Must be called with this folder's synchronization lock held. + * + * @return the IMAPProtocol for the Store's connection + * @exception ProtocolException for protocol errors + */ + protected synchronized IMAPProtocol getStoreProtocol() + throws ProtocolException { + connectionPoolLogger.fine("getStoreProtocol() borrowing a connection"); + return ((IMAPStore)store).getFolderStoreProtocol(); + } + + /** + * Throw the appropriate 'closed' exception. + * + * @param cex the ConnectionException + * @exception FolderClosedException if the folder is closed + * @exception StoreClosedException if the store is closed + */ + protected synchronized void throwClosedException(ConnectionException cex) + throws FolderClosedException, StoreClosedException { + // If it's the folder's protocol object, throw a FolderClosedException; + // otherwise, throw a StoreClosedException. + // If a command has failed because the connection is closed, + // the folder will have already been forced closed by the + // time we get here and our protocol object will have been + // released, so if we no longer have a protocol object we base + // this decision on whether we *think* the folder is open. + if ((protocol != null && cex.getProtocol() == protocol) || + (protocol == null && !reallyClosed)) + throw new FolderClosedException(this, cex.getMessage()); + else + throw new StoreClosedException(store, cex.getMessage()); + } + + /** + * Return the IMAPProtocol object for this folder.

+ * + * This method will block if necessary to wait for an IDLE + * command to finish. + * + * @return the IMAPProtocol object used when the folder is open + * @exception ProtocolException for protocol errors + */ + protected IMAPProtocol getProtocol() throws ProtocolException { + assert Thread.holdsLock(messageCacheLock); + waitIfIdle(); + // if we no longer have a protocol object after waiting, it probably + // means the connection has been closed due to a communnication error, + // or possibly because the folder has been closed + if (protocol == null) + throw new ConnectionException("Connection closed"); + return protocol; + } + + /** + * A simple interface for user-defined IMAP protocol commands. + */ + public static interface ProtocolCommand { + /** + * Execute the user-defined command using the supplied IMAPProtocol + * object. + * + * @param protocol the IMAPProtocol for the connection + * @return the results of the command + * @exception ProtocolException for protocol errors + */ + public Object doCommand(IMAPProtocol protocol) throws ProtocolException; + } + + /** + * Execute a user-supplied IMAP command. The command is executed + * in the appropriate context with the necessary locks held and + * using the appropriate IMAPProtocol object.

+ * + * This method returns whatever the ProtocolCommand + * object's doCommand method returns. If the + * doCommand method throws a ConnectionException + * it is translated into a StoreClosedException or + * FolderClosedException as appropriate. If the + * doCommand method throws a ProtocolException + * it is translated into a MessagingException.

+ * + * The following example shows how to execute the IMAP NOOP command. + * Executing more complex IMAP commands requires intimate knowledge + * of the com.sun.mail.iap and + * com.sun.mail.imap.protocol packages, best acquired by + * reading the source code. + * + *

+     * import com.sun.mail.iap.*;
+     * import com.sun.mail.imap.*;
+     * import com.sun.mail.imap.protocol.*;
+     *
+     * ...
+     *
+     * IMAPFolder f = (IMAPFolder)folder;
+     * Object val = f.doCommand(new IMAPFolder.ProtocolCommand() {
+     *	public Object doCommand(IMAPProtocol p)
+     *			throws ProtocolException {
+     *	    p.simpleCommand("NOOP", null);
+     *	    return null;
+     *	}
+     * });
+     * 
+ *

+ * + * Here's a more complex example showing how to use the proposed + * IMAP SORT extension: + * + *

+     * import com.sun.mail.iap.*;
+     * import com.sun.mail.imap.*;
+     * import com.sun.mail.imap.protocol.*;
+     *
+     * ...
+     *
+     * IMAPFolder f = (IMAPFolder)folder;
+     * Object val = f.doCommand(new IMAPFolder.ProtocolCommand() {
+     *	public Object doCommand(IMAPProtocol p)
+     *			throws ProtocolException {
+     *	    // Issue command
+     *	    Argument args = new Argument();
+     *	    Argument list = new Argument();
+     *	    list.writeString("SUBJECT");
+     *	    args.writeArgument(list);
+     *	    args.writeString("UTF-8");
+     *	    args.writeString("ALL");
+     *	    Response[] r = p.command("SORT", args);
+     *	    Response response = r[r.length-1];
+     *
+     *	    // Grab response
+     *	    Vector v = new Vector();
+     *	    if (response.isOK()) { // command succesful 
+     *		for (int i = 0, len = r.length; i < len; i++) {
+     *		    if (!(r[i] instanceof IMAPResponse))
+     *			continue;
+     *
+     *		    IMAPResponse ir = (IMAPResponse)r[i];
+     *		    if (ir.keyEquals("SORT")) {
+     *			String num;
+     *			while ((num = ir.readAtomString()) != null)
+     *			    System.out.println(num);
+     *			r[i] = null;
+     *		    }
+     *		}
+     *	    }
+     *
+     *	    // dispatch remaining untagged responses
+     *	    p.notifyResponseHandlers(r);
+     *	    p.handleResult(response);
+     *
+     *	    return null;
+     *	}
+     * });
+     * 
+ * + * @param cmd the protocol command + * @return the result of the command + * @exception MessagingException for failures + */ + public Object doCommand(ProtocolCommand cmd) throws MessagingException { + try { + return doProtocolCommand(cmd); + } catch (ConnectionException cex) { + // Oops, the store or folder died on us. + throwClosedException(cex); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + return null; + } + + public Object doOptionalCommand(String err, ProtocolCommand cmd) + throws MessagingException { + try { + return doProtocolCommand(cmd); + } catch (BadCommandException bex) { + throw new MessagingException(err, bex); + } catch (ConnectionException cex) { + // Oops, the store or folder died on us. + throwClosedException(cex); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + return null; + } + + public Object doCommandIgnoreFailure(ProtocolCommand cmd) + throws MessagingException { + try { + return doProtocolCommand(cmd); + } catch (CommandFailedException cfx) { + return null; + } catch (ConnectionException cex) { + // Oops, the store or folder died on us. + throwClosedException(cex); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + return null; + } + + protected synchronized Object doProtocolCommand(ProtocolCommand cmd) + throws ProtocolException { + /* + * Check whether we have a protocol object, not whether we're + * opened, to allow use of the exsting protocol object in the + * open method before the state is changed to "opened". + */ + if (protocol != null) { + synchronized (messageCacheLock) { + return cmd.doCommand(getProtocol()); + } + } + + // only get here if using store's connection + IMAPProtocol p = null; + + try { + p = getStoreProtocol(); + return cmd.doCommand(p); + } finally { + releaseStoreProtocol(p); + } + } + + /** + * Release the store protocol object. If we borrowed a protocol + * object from the connection pool, give it back. If we used our + * own protocol object, nothing to do. + * + * ASSERT: Must be called with this folder's synchronization lock held. + * + * @param p the IMAPProtocol object + */ + protected synchronized void releaseStoreProtocol(IMAPProtocol p) { + if (p != protocol) + ((IMAPStore)store).releaseFolderStoreProtocol(p); + else { + // XXX - should never happen + logger.fine("releasing our protocol as store protocol?"); + } + } + + /** + * Release the protocol object. + * + * ASSERT: This method must be called only when holding the + * messageCacheLock + * + * @param returnToPool return the protocol object to the pool? + */ + protected void releaseProtocol(boolean returnToPool) { + if (protocol != null) { + protocol.removeResponseHandler(this); + + if (returnToPool) + ((IMAPStore)store).releaseProtocol(this, protocol); + else { + protocol.disconnect(); // make sure it's disconnected + ((IMAPStore)store).releaseProtocol(this, null); + } + protocol = null; + } + } + + /** + * Issue a noop command for the connection if the connection has not been + * used in more than a second. If keepStoreAlive is true, + * also issue a noop over the store's connection. + * + * ASSERT: This method must be called only when holding the + * messageCacheLock + * + * @param keepStoreAlive keep the Store alive too? + * @exception ProtocolException for protocol errors + */ + protected void keepConnectionAlive(boolean keepStoreAlive) + throws ProtocolException { + + assert Thread.holdsLock(messageCacheLock); + if (protocol == null) // in case connection was closed + return; + if (System.currentTimeMillis() - protocol.getTimestamp() > 1000) { + waitIfIdle(); + if (protocol != null) + protocol.noop(); + } + + if (keepStoreAlive && ((IMAPStore)store).hasSeparateStoreConnection()) { + IMAPProtocol p = null; + try { + p = ((IMAPStore)store).getFolderStoreProtocol(); + if (System.currentTimeMillis() - p.getTimestamp() > 1000) + p.noop(); + } finally { + ((IMAPStore)store).releaseFolderStoreProtocol(p); + } + } + } + + /** + * Get the message object for the given sequence number. If + * none found, null is returned. + * + * ASSERT: This method must be called only when holding the + * messageCacheLock + * + * @param seqnum the message sequence number + * @return the IMAPMessage object + */ + protected IMAPMessage getMessageBySeqNumber(int seqnum) { + if (seqnum > messageCache.size()) { + // Microsoft Exchange will sometimes return message + // numbers that it has not yet notified the client + // about via EXISTS; ignore those messages here. + // GoDaddy IMAP does this too. + if (logger.isLoggable(Level.FINE)) + logger.fine("ignoring message number " + + seqnum + " outside range " + messageCache.size()); + return null; + } + return messageCache.getMessageBySeqnum(seqnum); + } + + /** + * Get the message objects for the given sequence numbers. + * + * ASSERT: This method must be called only when holding the + * messageCacheLock + * + * @param seqnums the array of message sequence numbers + * @return the IMAPMessage objects + * @since JavaMail 1.5.3 + */ + protected IMAPMessage[] getMessagesBySeqNumbers(int[] seqnums) { + IMAPMessage[] msgs = new IMAPMessage[seqnums.length]; + int nulls = 0; + // Map seq-numbers into actual Messages. + for (int i = 0; i < seqnums.length; i++) { + msgs[i] = getMessageBySeqNumber(seqnums[i]); + if (msgs[i] == null) + nulls++; + } + if (nulls > 0) { // compress the array to remove the nulls + IMAPMessage[] nmsgs = new IMAPMessage[seqnums.length - nulls]; + for (int i = 0, j = 0; i < msgs.length; i++) { + if (msgs[i] != null) + nmsgs[j++] = msgs[i]; + } + msgs = nmsgs; + } + return msgs; + } + + private boolean isDirectory() { + return ((type & HOLDS_FOLDERS) != 0); + } +} + +/** + * An object that holds a Message object + * and reports its size and writes it to another OutputStream + * on demand. Used by appendMessages to avoid the need to + * buffer the entire message in memory in a single byte array + * before sending it to the server. + */ +class MessageLiteral implements Literal { + private Message msg; + private int msgSize = -1; + private byte[] buf; // the buffered message, if not null + + public MessageLiteral(Message msg, int maxsize) + throws MessagingException, IOException { + this.msg = msg; + // compute the size here so exceptions can be returned immediately + LengthCounter lc = new LengthCounter(maxsize); + OutputStream os = new CRLFOutputStream(lc); + msg.writeTo(os); + os.flush(); + msgSize = lc.getSize(); + buf = lc.getBytes(); + } + + @Override + public int size() { + return msgSize; + } + + @Override + public void writeTo(OutputStream os) throws IOException { + // the message should not change between the constructor and this call + try { + if (buf != null) + os.write(buf, 0, msgSize); + else { + os = new CRLFOutputStream(os); + msg.writeTo(os); + } + } catch (MessagingException mex) { + // exceptions here are bad, "should" never happen + throw new IOException("MessagingException while appending message: " + + mex); + } + } +} + +/** + * Count the number of bytes written to the stream. + * Also, save a copy of small messages to avoid having to process + * the data again. + */ +class LengthCounter extends OutputStream { + private int size = 0; + private byte[] buf; + private int maxsize; + + public LengthCounter(int maxsize) { + buf = new byte[8192]; + this.maxsize = maxsize; + } + + @Override + public void write(int b) { + int newsize = size + 1; + if (buf != null) { + if (newsize > maxsize && maxsize >= 0) { + buf = null; + } else if (newsize > buf.length) { + byte newbuf[] = new byte[Math.max(buf.length << 1, newsize)]; + System.arraycopy(buf, 0, newbuf, 0, size); + buf = newbuf; + buf[size] = (byte)b; + } else { + buf[size] = (byte)b; + } + } + size = newsize; + } + + @Override + public void write(byte b[], int off, int len) { + if ((off < 0) || (off > b.length) || (len < 0) || + ((off + len) > b.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + int newsize = size + len; + if (buf != null) { + if (newsize > maxsize && maxsize >= 0) { + buf = null; + } else if (newsize > buf.length) { + byte newbuf[] = new byte[Math.max(buf.length << 1, newsize)]; + System.arraycopy(buf, 0, newbuf, 0, size); + buf = newbuf; + System.arraycopy(b, off, buf, size, len); + } else { + System.arraycopy(b, off, buf, size, len); + } + } + size = newsize; + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + public int getSize() { + return size; + } + + public byte[] getBytes() { + return buf; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IMAPInputStream.java b/app/src/main/java/com/sun/mail/imap/IMAPInputStream.java new file mode 100644 index 0000000000..52018e288b --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IMAPInputStream.java @@ -0,0 +1,301 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.*; +import javax.mail.*; +import com.sun.mail.imap.protocol.*; +import com.sun.mail.iap.*; +import com.sun.mail.util.FolderClosedIOException; +import com.sun.mail.util.MessageRemovedIOException; + +/** + * This class implements an IMAP data stream. + * + * @author John Mani + */ + +public class IMAPInputStream extends InputStream { + private IMAPMessage msg; // this message + private String section; // section-id + private int pos; // track the position within the IMAP datastream + private int blksize; // number of bytes to read in each FETCH request + private int max; // the total number of bytes in this section. + // -1 indicates unknown + private byte[] buf; // the buffer obtained from fetchBODY() + private int bufcount; // The index one greater than the index of the + // last valid byte in 'buf' + private int bufpos; // The current position within 'buf' + private boolean lastBuffer; // is this the last buffer of data? + private boolean peek; // peek instead of fetch? + private ByteArray readbuf; // reuse for each read + + // Allocate this much extra space in the read buffer to allow + // space for the FETCH response overhead + private static final int slop = 64; + + + /** + * Create an IMAPInputStream. + * + * @param msg the IMAPMessage the data will come from + * @param section the IMAP section/part identifier for the data + * @param max the number of bytes in this section + * @param peek peek instead of fetch? + */ + public IMAPInputStream(IMAPMessage msg, String section, int max, + boolean peek) { + this.msg = msg; + this.section = section; + this.max = max; + this.peek = peek; + pos = 0; + blksize = msg.getFetchBlockSize(); + } + + /** + * Do a NOOP to force any untagged EXPUNGE responses + * and then check if this message is expunged. + */ + private void forceCheckExpunged() + throws MessageRemovedIOException, FolderClosedIOException { + synchronized (msg.getMessageCacheLock()) { + try { + msg.getProtocol().noop(); + } catch (ConnectionException cex) { + throw new FolderClosedIOException(msg.getFolder(), + cex.getMessage()); + } catch (FolderClosedException fex) { + throw new FolderClosedIOException(fex.getFolder(), + fex.getMessage()); + } catch (ProtocolException pex) { + // ignore it + } + } + if (msg.isExpunged()) + throw new MessageRemovedIOException(); + } + + /** + * Fetch more data from the server. This method assumes that all + * data has already been read in, hence bufpos > bufcount. + */ + private void fill() throws IOException { + /* + * If we've read the last buffer, there's no more to read. + * If we know the total number of bytes available from this + * section, let's check if we have consumed that many bytes. + */ + if (lastBuffer || max != -1 && pos >= max) { + if (pos == 0) + checkSeen(); + readbuf = null; // XXX - return to pool? + return; // the caller of fill() will return -1. + } + + BODY b = null; + if (readbuf == null) + readbuf = new ByteArray(blksize + slop); + + ByteArray ba; + int cnt; + // Acquire MessageCacheLock, to freeze seqnum. + synchronized (msg.getMessageCacheLock()) { + try { + IMAPProtocol p = msg.getProtocol(); + + // Check whether this message is expunged + if (msg.isExpunged()) + throw new MessageRemovedIOException( + "No content for expunged message"); + + int seqnum = msg.getSequenceNumber(); + cnt = blksize; + if (max != -1 && pos + blksize > max) + cnt = max - pos; + if (peek) + b = p.peekBody(seqnum, section, pos, cnt, readbuf); + else + b = p.fetchBody(seqnum, section, pos, cnt, readbuf); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new IOException(pex.getMessage()); + } catch (FolderClosedException fex) { + throw new FolderClosedIOException(fex.getFolder(), + fex.getMessage()); + } + + if (b == null || ((ba = b.getByteArray()) == null)) { + forceCheckExpunged(); + // nope, the server doesn't think it's expunged. + // can't tell the difference between the server returning NIL + // and some other error that caused null to be returned above, + // so we'll just assume it was empty content. + ba = new ByteArray(0); + } + } + + // make sure the SEEN flag is set after reading the first chunk + if (pos == 0) + checkSeen(); + + // setup new values .. + buf = ba.getBytes(); + bufpos = ba.getStart(); + int n = ba.getCount(); // will be zero, if all data has been + // consumed from the server. + + int origin = b != null ? b.getOrigin() : pos; + if (origin < 0) { + /* + * Some versions of Exchange will return the entire message + * body even though we only ask for a chunk, and the returned + * data won't specify an "origin". If this happens, and we + * get more data than we asked for, assume it's the entire + * message body. + */ + if (pos == 0) { + /* + * If we got more or less than we asked for, + * this is the last buffer of data. + */ + lastBuffer = n != cnt; + } else { + /* + * We asked for data NOT starting at the beginning, + * but we got back data starting at the beginning. + * Possibly we could extract the needed data from + * some part of the data we got back, but really this + * should never happen so we just assume something is + * broken and terminate the data here. + */ + n = 0; + lastBuffer = true; + } + } else if (origin == pos) { + /* + * If we got less than we asked for, + * this is the last buffer of data. + */ + lastBuffer = n < cnt; + } else { + /* + * We got back data that doesn't match the request. + * Just terminate the data here. + */ + n = 0; + lastBuffer = true; + } + + bufcount = bufpos + n; + pos += n; + + } + + /** + * Reads the next byte of data from this buffered input stream. + * If no byte is available, the value -1 is returned. + */ + @Override + public synchronized int read() throws IOException { + if (bufpos >= bufcount) { + fill(); + if (bufpos >= bufcount) + return -1; // EOF + } + return buf[bufpos++] & 0xff; + } + + /** + * Reads up to len bytes of data from this + * input stream into the given buffer.

+ * + * Returns the total number of bytes read into the buffer, + * or -1 if there is no more data.

+ * + * Note that this method mimics the "weird !" semantics of + * BufferedInputStream in that the number of bytes actually + * returned may be less that the requested value. So callers + * of this routine should be aware of this and must check + * the return value to insure that they have obtained the + * requisite number of bytes. + */ + @Override + public synchronized int read(byte b[], int off, int len) + throws IOException { + + int avail = bufcount - bufpos; + if (avail <= 0) { + fill(); + avail = bufcount - bufpos; + if (avail <= 0) + return -1; // EOF + } + int cnt = (avail < len) ? avail : len; + System.arraycopy(buf, bufpos, b, off, cnt); + bufpos += cnt; + return cnt; + } + + /** + * Reads up to b.length bytes of data from this input + * stream into an array of bytes.

+ * + * Returns the total number of bytes read into the buffer, or + * -1 is there is no more data.

+ * + * Note that this method mimics the "weird !" semantics of + * BufferedInputStream in that the number of bytes actually + * returned may be less that the requested value. So callers + * of this routine should be aware of this and must check + * the return value to insure that they have obtained the + * requisite number of bytes. + */ + @Override + public int read(byte b[]) throws IOException { + return read(b, 0, b.length); + } + + /** + * Returns the number of bytes that can be read from this input + * stream without blocking. + */ + @Override + public synchronized int available() throws IOException { + return (bufcount - bufpos); + } + + /** + * Normally the SEEN flag will have been set by now, but if not, + * force it to be set (as long as the folder isn't open read-only + * and we're not peeking). + * And of course, if there's no folder (e.g., a nested message) + * don't do anything. + */ + private void checkSeen() { + if (peek) // if we're peeking, don't set the SEEN flag + return; + try { + Folder f = msg.getFolder(); + if (f != null && f.getMode() != Folder.READ_ONLY && + !msg.isSet(Flags.Flag.SEEN)) + msg.setFlag(Flags.Flag.SEEN, true); + } catch (MessagingException ex) { + // ignore it + } + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IMAPMessage.java b/app/src/main/java/com/sun/mail/imap/IMAPMessage.java new file mode 100644 index 0000000000..453479d989 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IMAPMessage.java @@ -0,0 +1,1700 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.util.Date; +import java.io.*; +import java.util.*; + +import javax.mail.*; +import javax.mail.internet.*; +import javax.activation.*; + +import com.sun.mail.util.ReadableMime; +import com.sun.mail.iap.*; +import com.sun.mail.imap.protocol.*; + +/** + * This class implements an IMAPMessage object.

+ * + * An IMAPMessage object starts out as a light-weight object. It gets + * filled-in incrementally when a request is made for some item. Or + * when a prefetch is done using the FetchProfile.

+ * + * An IMAPMessage has a messageNumber and a sequenceNumber. The + * messageNumber is its index into its containing folder's messageCache. + * The sequenceNumber is its IMAP sequence-number. + * + * @author John Mani + * @author Bill Shannon + */ +/* + * The lock hierarchy is that the lock on the IMAPMessage object, if + * it's acquired at all, must be acquired before the message cache lock. + * The IMAPMessage lock protects the message flags, sort of. + * + * XXX - I'm not convinced that all fields of IMAPMessage are properly + * protected by locks. + */ + +public class IMAPMessage extends MimeMessage implements ReadableMime { + protected BODYSTRUCTURE bs; // BODYSTRUCTURE + protected ENVELOPE envelope; // ENVELOPE + + /** + * A map of the extension FETCH items. In addition to saving the + * data in this map, an entry in this map indicates that we *have* + * the data, and so it doesn't need to be fetched again. The map + * is created only when needed, to avoid significantly increasing + * the effective size of an IMAPMessage object. + * + * @since JavaMail 1.4.6 + */ + protected Map items; // Map + + private Date receivedDate; // INTERNALDATE + private long size = -1; // RFC822.SIZE + + private Boolean peek; // use BODY.PEEK when fetching content? + + // this message's IMAP UID + private volatile long uid = -1; + + // this message's IMAP MODSEQ - RFC 4551 CONDSTORE + private volatile long modseq = -1; + + // this message's IMAP sectionId (null for toplevel message, + // non-null for a nested message) + protected String sectionId; + + // processed values + private String type; // Content-Type (with params) + private String subject; // decoded (Unicode) subject + private String description; // decoded (Unicode) desc + + // Indicates that we've loaded *all* headers for this message + private volatile boolean headersLoaded = false; + + // Indicates that we've cached the body of this message + private volatile boolean bodyLoaded = false; + + /* Hashtable of names of headers we've loaded from the server. + * Used in isHeaderLoaded() and getHeaderLoaded() to keep track + * of those headers we've attempted to load from the server. We + * need this table of names to avoid multiple attempts at loading + * headers that don't exist for a particular message. + * + * Could this somehow be included in the InternetHeaders object ?? + */ + private Hashtable loadedHeaders + = new Hashtable<>(1); + + // This is our Envelope + static final String EnvelopeCmd = "ENVELOPE INTERNALDATE RFC822.SIZE"; + + /** + * Constructor. + * + * @param folder the folder containing this message + * @param msgnum the message sequence number + */ + protected IMAPMessage(IMAPFolder folder, int msgnum) { + super(folder, msgnum); + flags = null; + } + + /** + * Constructor, for use by IMAPNestedMessage. + * + * @param session the Session + */ + protected IMAPMessage(Session session) { + super(session); + } + + /** + * Get this message's folder's protocol connection. + * Throws FolderClosedException, if the protocol connection + * is not available. + * + * ASSERT: Must hold the messageCacheLock. + * + * @return the IMAPProtocol object for the containing folder + * @exception ProtocolException for protocol errors + * @exception FolderClosedException if the folder is closed + */ + protected IMAPProtocol getProtocol() + throws ProtocolException, FolderClosedException { + ((IMAPFolder)folder).waitIfIdle(); + IMAPProtocol p = ((IMAPFolder)folder).protocol; + if (p == null) + throw new FolderClosedException(folder); + else + return p; + } + + /* + * Is this an IMAP4 REV1 server? + */ + protected boolean isREV1() throws FolderClosedException { + // access the folder's protocol object without waiting + // for IDLE to complete + IMAPProtocol p = ((IMAPFolder)folder).protocol; + if (p == null) + throw new FolderClosedException(folder); + else + return p.isREV1(); + } + + /** + * Get the messageCacheLock, associated with this Message's + * Folder. + * + * @return the message cache lock object + */ + protected Object getMessageCacheLock() { + return ((IMAPFolder)folder).messageCacheLock; + } + + /** + * Get this message's IMAP sequence number. + * + * ASSERT: This method must be called only when holding the + * messageCacheLock. + * + * @return the message sequence number + */ + protected int getSequenceNumber() { + return ((IMAPFolder)folder).messageCache.seqnumOf(getMessageNumber()); + } + + /** + * Wrapper around the protected method Message.setMessageNumber() to + * make that method accessible to IMAPFolder. + */ + @Override + protected void setMessageNumber(int msgnum) { + super.setMessageNumber(msgnum); + } + + /** + * Return the UID for this message. + * Returns -1 if not known; use UIDFolder.getUID() in this case. + * + * @return the UID + * @see javax.mail.UIDFolder#getUID + */ + protected long getUID() { + return uid; + } + + protected void setUID(long uid) { + this.uid = uid; + } + + /** + * Return the modification sequence number (MODSEQ) for this message. + * Returns -1 if not known. + * + * @return the modification sequence number + * @exception MessagingException for failures + * @see "RFC 4551" + * @since JavaMail 1.5.1 + */ + public synchronized long getModSeq() throws MessagingException { + if (modseq != -1) + return modseq; + + synchronized (getMessageCacheLock()) { // Acquire Lock + try { + IMAPProtocol p = getProtocol(); + checkExpunged(); // insure that message is not expunged + MODSEQ ms = p.fetchMODSEQ(getSequenceNumber()); + + if (ms != null) + modseq = ms.modseq; + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + return modseq; + } + + long _getModSeq() { + return modseq; + } + + void setModSeq(long modseq) { + this.modseq = modseq; + } + + // expose to MessageCache + @Override + protected void setExpunged(boolean set) { + super.setExpunged(set); + } + + // Convenience routine + protected void checkExpunged() throws MessageRemovedException { + if (expunged) + throw new MessageRemovedException(); + } + + /** + * Do a NOOP to force any untagged EXPUNGE responses + * and then check if this message is expunged. + * + * @exception MessageRemovedException if the message has been removed + * @exception FolderClosedException if the folder has been closed + */ + protected void forceCheckExpunged() + throws MessageRemovedException, FolderClosedException { + synchronized (getMessageCacheLock()) { + try { + getProtocol().noop(); + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + // ignore it + } + } + if (expunged) + throw new MessageRemovedException(); + } + + // Return the block size for FETCH requests + // MUST be overridden by IMAPNestedMessage + protected int getFetchBlockSize() { + return ((IMAPStore)folder.getStore()).getFetchBlockSize(); + } + + // Should we ignore the size in the BODYSTRUCTURE? + // MUST be overridden by IMAPNestedMessage + protected boolean ignoreBodyStructureSize() { + return ((IMAPStore)folder.getStore()).ignoreBodyStructureSize(); + } + + /** + * Get the "From" attribute. + */ + @Override + public Address[] getFrom() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getFrom(); + loadEnvelope(); + InternetAddress[] a = envelope.from; + /* + * Per RFC 2822, the From header is required, and thus the IMAP + * spec also requires that it be present, but we know that in + * practice it is often missing. Some servers fill in the + * From field with the Sender field in this case, but at least + * Exchange 2007 does not. Use the same fallback strategy used + * by MimeMessage. + */ + if (a == null || a.length == 0) + a = envelope.sender; + return aaclone(a); + } + + @Override + public void setFrom(Address address) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + @Override + public void addFrom(Address[] addresses) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the "Sender" attribute. + */ + @Override + public Address getSender() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getSender(); + loadEnvelope(); + if (envelope.sender != null && envelope.sender.length > 0) + return (envelope.sender)[0]; // there can be only one sender + else + return null; + } + + + @Override + public void setSender(Address address) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the desired Recipient type. + */ + @Override + public Address[] getRecipients(Message.RecipientType type) + throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getRecipients(type); + loadEnvelope(); + + if (type == Message.RecipientType.TO) + return aaclone(envelope.to); + else if (type == Message.RecipientType.CC) + return aaclone(envelope.cc); + else if (type == Message.RecipientType.BCC) + return aaclone(envelope.bcc); + else + return super.getRecipients(type); + } + + @Override + public void setRecipients(Message.RecipientType type, Address[] addresses) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + @Override + public void addRecipients(Message.RecipientType type, Address[] addresses) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the ReplyTo addresses. + */ + @Override + public Address[] getReplyTo() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getReplyTo(); + loadEnvelope(); + /* + * The IMAP spec requires that the Reply-To field never be + * null, but at least Exchange 2007 fails to fill it in in + * some cases. Use the same fallback strategy used by + * MimeMessage. + */ + if (envelope.replyTo == null || envelope.replyTo.length == 0) + return getFrom(); + return aaclone(envelope.replyTo); + } + + @Override + public void setReplyTo(Address[] addresses) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the decoded subject. + */ + @Override + public String getSubject() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getSubject(); + + if (subject != null) // already cached ? + return subject; + + loadEnvelope(); + if (envelope.subject == null) // no subject + return null; + + // Cache and return the decoded value. + try { + // The server *should* unfold the value, but just in case it + // doesn't we unfold it here. + subject = + MimeUtility.decodeText(MimeUtility.unfold(envelope.subject)); + } catch (UnsupportedEncodingException ex) { + subject = envelope.subject; + } + + return subject; + } + + @Override + public void setSubject(String subject, String charset) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the SentDate. + */ + @Override + public Date getSentDate() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getSentDate(); + loadEnvelope(); + if (envelope.date == null) + return null; + else + return new Date(envelope.date.getTime()); + } + + @Override + public void setSentDate(Date d) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the received date (INTERNALDATE). + */ + @Override + public Date getReceivedDate() throws MessagingException { + checkExpunged(); + if (receivedDate == null) + loadEnvelope(); // have to go to the server for this + if (receivedDate == null) + return null; + else + return new Date(receivedDate.getTime()); + } + + /** + * Get the message size.

+ * + * Note that this returns RFC822.SIZE. That is, it's the + * size of the whole message, header and body included. + * Note also that if the size of the message is greater than + * Integer.MAX_VALUE (2GB), this method returns Integer.MAX_VALUE. + */ + @Override + public int getSize() throws MessagingException { + checkExpunged(); + // if bodyLoaded, size is already set + if (size == -1) + loadEnvelope(); // XXX - could just fetch the size + if (size > Integer.MAX_VALUE) + return Integer.MAX_VALUE; // the best we can do... + else + return (int)size; + } + + /** + * Get the message size as a long.

+ * + * Suitable for messages that might be larger than 2GB. + * @return the message size as a long integer + * @exception MessagingException for failures + * @since JavaMail 1.6 + */ + public long getSizeLong() throws MessagingException { + checkExpunged(); + // if bodyLoaded, size is already set + if (size == -1) + loadEnvelope(); // XXX - could just fetch the size + return size; + } + + /** + * Get the total number of lines.

+ * + * Returns the "body_fld_lines" field from the + * BODYSTRUCTURE. Note that this field is available + * only for text/plain and message/rfc822 types + */ + @Override + public int getLineCount() throws MessagingException { + checkExpunged(); + // XXX - superclass doesn't implement this + loadBODYSTRUCTURE(); + return bs.lines; + } + + /** + * Get the content language. + */ + @Override + public String[] getContentLanguage() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getContentLanguage(); + loadBODYSTRUCTURE(); + if (bs.language != null) + return bs.language.clone(); + else + return null; + } + + @Override + public void setContentLanguage(String[] languages) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the In-Reply-To header. + * + * @return the In-Reply-To header + * @exception MessagingException for failures + * @since JavaMail 1.3.3 + */ + public String getInReplyTo() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getHeader("In-Reply-To", " "); + loadEnvelope(); + return envelope.inReplyTo; + } + + /** + * Get the Content-Type. + * + * Generate this header from the BODYSTRUCTURE. Append parameters + * as well. + */ + @Override + public synchronized String getContentType() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getContentType(); + + // If we haven't cached the type yet .. + if (type == null) { + loadBODYSTRUCTURE(); + // generate content-type from BODYSTRUCTURE + ContentType ct = new ContentType(bs.type, bs.subtype, bs.cParams); + type = ct.toString(); + } + return type; + } + + /** + * Get the Content-Disposition. + */ + @Override + public String getDisposition() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getDisposition(); + loadBODYSTRUCTURE(); + return bs.disposition; + } + + @Override + public void setDisposition(String disposition) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the Content-Transfer-Encoding. + */ + @Override + public String getEncoding() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getEncoding(); + loadBODYSTRUCTURE(); + return bs.encoding; + } + + /** + * Get the Content-ID. + */ + @Override + public String getContentID() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getContentID(); + loadBODYSTRUCTURE(); + return bs.id; + } + + @Override + public void setContentID(String cid) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the Content-MD5. + */ + @Override + public String getContentMD5() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getContentMD5(); + loadBODYSTRUCTURE(); + return bs.md5; + } + + @Override + public void setContentMD5(String md5) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the decoded Content-Description. + */ + @Override + public String getDescription() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getDescription(); + + if (description != null) // cached value ? + return description; + + loadBODYSTRUCTURE(); + if (bs.description == null) + return null; + + try { + description = MimeUtility.decodeText(bs.description); + } catch (UnsupportedEncodingException ex) { + description = bs.description; + } + + return description; + } + + @Override + public void setDescription(String description, String charset) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the Message-ID. + */ + @Override + public String getMessageID() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getMessageID(); + loadEnvelope(); + return envelope.messageId; + } + + /** + * Get the "filename" Disposition parameter. (Only available in + * IMAP4rev1). If thats not available, get the "name" ContentType + * parameter. + */ + @Override + public String getFileName() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getFileName(); + + String filename = null; + loadBODYSTRUCTURE(); + + if (bs.dParams != null) + filename = bs.dParams.get("filename"); + if (filename == null && bs.cParams != null) + filename = bs.cParams.get("name"); + return filename; + } + + @Override + public void setFileName(String filename) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get all the bytes for this message. Overrides getContentStream() + * in MimeMessage. This method is ultimately used by the DataHandler + * to obtain the input stream for this message. + * + * @see javax.mail.internet.MimeMessage#getContentStream + */ + @Override + protected InputStream getContentStream() throws MessagingException { + if (bodyLoaded) + return super.getContentStream(); + InputStream is = null; + boolean pk = getPeek(); // get before acquiring message cache lock + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + + // This message could be expunged when we were waiting + // to acquire the lock ... + checkExpunged(); + + if (p.isREV1() && (getFetchBlockSize() != -1)) // IMAP4rev1 + return new IMAPInputStream(this, toSection("TEXT"), + bs != null && !ignoreBodyStructureSize() ? + bs.size : -1, pk); + + if (p.isREV1()) { + BODY b; + if (pk) + b = p.peekBody(getSequenceNumber(), toSection("TEXT")); + else + b = p.fetchBody(getSequenceNumber(), toSection("TEXT")); + if (b != null) + is = b.getByteArrayInputStream(); + } else { + RFC822DATA rd = p.fetchRFC822(getSequenceNumber(), "TEXT"); + if (rd != null) + is = rd.getByteArrayInputStream(); + } + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + } + + if (is == null) { + forceCheckExpunged(); // may throw MessageRemovedException + // nope, the server doesn't think it's expunged. + // can't tell the difference between the server returning NIL + // and some other error that caused null to be returned above, + // so we'll just assume it was empty content. + is = new ByteArrayInputStream(new byte[0]); + } + return is; + } + + /** + * Get the DataHandler object for this message. + */ + @Override + public synchronized DataHandler getDataHandler() + throws MessagingException { + checkExpunged(); + + if (dh == null && !bodyLoaded) { + loadBODYSTRUCTURE(); + if (type == null) { // type not yet computed + // generate content-type from BODYSTRUCTURE + ContentType ct = new ContentType(bs.type, bs.subtype, + bs.cParams); + type = ct.toString(); + } + + /* Special-case Multipart and Nested content. All other + * cases are handled by the superclass. + */ + if (bs.isMulti()) + dh = new DataHandler( + new IMAPMultipartDataSource(this, bs.bodies, + sectionId, this) + ); + else if (bs.isNested() && isREV1() && bs.envelope != null) + /* Nested messages are handled specially only for + * IMAP4rev1. IMAP4 doesn't provide enough support to + * FETCH the components of nested messages + */ + dh = new DataHandler( + new IMAPNestedMessage(this, + bs.bodies[0], + bs.envelope, + sectionId == null ? "1" : sectionId + ".1"), + type + ); + } + + return super.getDataHandler(); + } + + @Override + public void setDataHandler(DataHandler content) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Return the MIME format stream corresponding to this message. + * + * @return the MIME format stream + * @since JavaMail 1.4.5 + */ + @Override + public InputStream getMimeStream() throws MessagingException { + // XXX - need an "if (bodyLoaded)" version + InputStream is = null; + boolean pk = getPeek(); // get before acquiring message cache lock + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + + checkExpunged(); // insure this message is not expunged + + if (p.isREV1() && (getFetchBlockSize() != -1)) // IMAP4rev1 + return new IMAPInputStream(this, sectionId, -1, pk); + + if (p.isREV1()) { + BODY b; + if (pk) + b = p.peekBody(getSequenceNumber(), sectionId); + else + b = p.fetchBody(getSequenceNumber(), sectionId); + if (b != null) + is = b.getByteArrayInputStream(); + } else { + RFC822DATA rd = p.fetchRFC822(getSequenceNumber(), null); + if (rd != null) + is = rd.getByteArrayInputStream(); + } + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + } + + if (is == null) { + forceCheckExpunged(); // may throw MessageRemovedException + // nope, the server doesn't think it's expunged. + // can't tell the difference between the server returning NIL + // and some other error that caused null to be returned above, + // so we'll just assume it was empty content. + is = new ByteArrayInputStream(new byte[0]); + } + return is; + } + + /** + * Write out the bytes into the given OutputStream. + */ + @Override + public void writeTo(OutputStream os) + throws IOException, MessagingException { + if (bodyLoaded) { + super.writeTo(os); + return; + } + InputStream is = getMimeStream(); + try { + // write out the bytes + byte[] bytes = new byte[16*1024]; + int count; + while ((count = is.read(bytes)) != -1) + os.write(bytes, 0, count); + } finally { + is.close(); + } + } + + /** + * Get the named header. + */ + @Override + public String[] getHeader(String name) throws MessagingException { + checkExpunged(); + + if (isHeaderLoaded(name)) // already loaded ? + return headers.getHeader(name); + + // Load this particular header + InputStream is = null; + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + + // This message could be expunged when we were waiting + // to acquire the lock ... + checkExpunged(); + + if (p.isREV1()) { + BODY b = p.peekBody(getSequenceNumber(), + toSection("HEADER.FIELDS (" + name + ")") + ); + if (b != null) + is = b.getByteArrayInputStream(); + } else { + RFC822DATA rd = p.fetchRFC822(getSequenceNumber(), + "HEADER.LINES (" + name + ")"); + if (rd != null) + is = rd.getByteArrayInputStream(); + } + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + } + + // if we get this far without "is" being set, something has gone + // wrong; prevent a later NullPointerException and return null here + if (is == null) + return null; + + if (headers == null) + headers = new InternetHeaders(); + headers.load(is); // load this header into the Headers object. + setHeaderLoaded(name); // Mark this header as loaded + + return headers.getHeader(name); + } + + /** + * Get the named header. + */ + @Override + public String getHeader(String name, String delimiter) + throws MessagingException { + checkExpunged(); + + // force the header to be loaded by invoking getHeader(name) + if (getHeader(name) == null) + return null; + return headers.getHeader(name, delimiter); + } + + @Override + public void setHeader(String name, String value) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + @Override + public void addHeader(String name, String value) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + @Override + public void removeHeader(String name) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get all headers. + */ + @Override + public Enumeration

getAllHeaders() throws MessagingException { + checkExpunged(); + loadHeaders(); + return super.getAllHeaders(); + } + + /** + * Get matching headers. + */ + @Override + public Enumeration
getMatchingHeaders(String[] names) + throws MessagingException { + checkExpunged(); + loadHeaders(); + return super.getMatchingHeaders(names); + } + + /** + * Get non-matching headers. + */ + @Override + public Enumeration
getNonMatchingHeaders(String[] names) + throws MessagingException { + checkExpunged(); + loadHeaders(); + return super.getNonMatchingHeaders(names); + } + + @Override + public void addHeaderLine(String line) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get all header-lines. + */ + @Override + public Enumeration getAllHeaderLines() throws MessagingException { + checkExpunged(); + loadHeaders(); + return super.getAllHeaderLines(); + } + + /** + * Get all matching header-lines. + */ + @Override + public Enumeration getMatchingHeaderLines(String[] names) + throws MessagingException { + checkExpunged(); + loadHeaders(); + return super.getMatchingHeaderLines(names); + } + + /** + * Get all non-matching headerlines. + */ + @Override + public Enumeration getNonMatchingHeaderLines(String[] names) + throws MessagingException { + checkExpunged(); + loadHeaders(); + return super.getNonMatchingHeaderLines(names); + } + + /** + * Get the Flags for this message. + */ + @Override + public synchronized Flags getFlags() throws MessagingException { + checkExpunged(); + loadFlags(); + return super.getFlags(); + } + + /** + * Test if the given Flags are set in this message. + */ + @Override + public synchronized boolean isSet(Flags.Flag flag) + throws MessagingException { + checkExpunged(); + loadFlags(); + return super.isSet(flag); + } + + /** + * Set/Unset the given flags in this message. + */ + @Override + public synchronized void setFlags(Flags flag, boolean set) + throws MessagingException { + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + checkExpunged(); // Insure that this message is not expunged + p.storeFlags(getSequenceNumber(), flag, set); + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } + + /** + * Set whether or not to use the PEEK variant of FETCH when + * fetching message content. This overrides the default + * value from the "mail.imap.peek" property. + * + * @param peek the peek flag + * @since JavaMail 1.3.3 + */ + public synchronized void setPeek(boolean peek) { + this.peek = Boolean.valueOf(peek); + } + + /** + * Get whether or not to use the PEEK variant of FETCH when + * fetching message content. + * + * @return the peek flag + * @since JavaMail 1.3.3 + */ + public synchronized boolean getPeek() { + if (peek == null) + return ((IMAPStore)folder.getStore()).getPeek(); + else + return peek.booleanValue(); + } + + /** + * Invalidate cached header and envelope information for this + * message. Subsequent accesses of this information will + * cause it to be fetched from the server. + * + * @since JavaMail 1.3.3 + */ + public synchronized void invalidateHeaders() { + headersLoaded = false; + loadedHeaders.clear(); + headers = null; + envelope = null; + bs = null; + receivedDate = null; + size = -1; + type = null; + subject = null; + description = null; + flags = null; + content = null; + contentStream = null; + bodyLoaded = false; + } + + /** + * This class implements the test to be done on each + * message in the folder. The test is to check whether the + * message has already cached all the items requested in the + * FetchProfile. If any item is missing, the test succeeds and + * breaks out. + */ + public static class FetchProfileCondition implements Utility.Condition { + private boolean needEnvelope = false; + private boolean needFlags = false; + private boolean needBodyStructure = false; + private boolean needUID = false; + private boolean needHeaders = false; + private boolean needSize = false; + private boolean needMessage = false; + private boolean needRDate = false; + private String[] hdrs = null; + private Set need = new HashSet<>(); + + /** + * Create a FetchProfileCondition to determine if we need to fetch + * any of the information specified in the FetchProfile. + * + * @param fp the FetchProfile + * @param fitems the FETCH items + */ + @SuppressWarnings("deprecation") // for FetchProfile.Item.SIZE + public FetchProfileCondition(FetchProfile fp, FetchItem[] fitems) { + if (fp.contains(FetchProfile.Item.ENVELOPE)) + needEnvelope = true; + if (fp.contains(FetchProfile.Item.FLAGS)) + needFlags = true; + if (fp.contains(FetchProfile.Item.CONTENT_INFO)) + needBodyStructure = true; + if (fp.contains(FetchProfile.Item.SIZE)) + needSize = true; + if (fp.contains(UIDFolder.FetchProfileItem.UID)) + needUID = true; + if (fp.contains(IMAPFolder.FetchProfileItem.HEADERS)) + needHeaders = true; + if (fp.contains(IMAPFolder.FetchProfileItem.SIZE)) + needSize = true; + if (fp.contains(IMAPFolder.FetchProfileItem.MESSAGE)) + needMessage = true; + if (fp.contains(IMAPFolder.FetchProfileItem.INTERNALDATE)) + needRDate = true; + hdrs = fp.getHeaderNames(); + for (int i = 0; i < fitems.length; i++) { + if (fp.contains(fitems[i].getFetchProfileItem())) + need.add(fitems[i]); + } + } + + /** + * Return true if we NEED to fetch the requested information + * for the specified message. + */ + @Override + public boolean test(IMAPMessage m) { + if (needEnvelope && m._getEnvelope() == null && !m.bodyLoaded) + return true; // no envelope + if (needFlags && m._getFlags() == null) + return true; // no flags + if (needBodyStructure && m._getBodyStructure() == null && + !m.bodyLoaded) + return true; // no BODYSTRUCTURE + if (needUID && m.getUID() == -1) // no UID + return true; + if (needHeaders && !m.areHeadersLoaded()) // no headers + return true; + if (needSize && m.size == -1 && !m.bodyLoaded) // no size + return true; + if (needMessage && !m.bodyLoaded) // no message body + return true; + if (needRDate && m.receivedDate == null) // no received date + return true; + + // Is the desired header present ? + for (int i = 0; i < hdrs.length; i++) { + if (!m.isHeaderLoaded(hdrs[i])) + return true; // Nope, return + } + Iterator it = need.iterator(); + while (it.hasNext()) { + FetchItem fitem = it.next(); + if (m.items == null || m.items.get(fitem.getName()) == null) + return true; + } + + return false; + } + } + + /** + * Apply the data in the FETCH item to this message. + * + * ASSERT: Must hold the messageCacheLock. + * + * @param item the fetch item + * @param hdrs the headers we're asking for + * @param allHeaders load all headers? + * @return did we handle this fetch item? + * @exception MessagingException for failures + * @since JavaMail 1.4.6 + */ + protected boolean handleFetchItem(Item item, + String[] hdrs, boolean allHeaders) + throws MessagingException { + // Check for the FLAGS item + if (item instanceof Flags) + flags = (Flags)item; + // Check for ENVELOPE items + else if (item instanceof ENVELOPE) + envelope = (ENVELOPE)item; + else if (item instanceof INTERNALDATE) + receivedDate = ((INTERNALDATE)item).getDate(); + else if (item instanceof RFC822SIZE) + size = ((RFC822SIZE)item).size; + else if (item instanceof MODSEQ) + modseq = ((MODSEQ)item).modseq; + + // Check for the BODYSTRUCTURE item + else if (item instanceof BODYSTRUCTURE) + bs = (BODYSTRUCTURE)item; + // Check for the UID item + else if (item instanceof UID) { + UID u = (UID)item; + uid = u.uid; // set uid + // add entry into uid table + if (((IMAPFolder)folder).uidTable == null) + ((IMAPFolder) folder).uidTable + = new Hashtable<>(); + ((IMAPFolder)folder).uidTable.put(Long.valueOf(u.uid), this); + } + + // Check for header items + else if (item instanceof RFC822DATA || + item instanceof BODY) { + InputStream headerStream; + boolean isHeader; + if (item instanceof RFC822DATA) { // IMAP4 + headerStream = + ((RFC822DATA)item).getByteArrayInputStream(); + isHeader = ((RFC822DATA)item).isHeader(); + } else { // IMAP4rev1 + headerStream = + ((BODY)item).getByteArrayInputStream(); + isHeader = ((BODY)item).isHeader(); + } + + if (!isHeader) { + // load the entire message by using the superclass + // MimeMessage.parse method + // first, save the size of the message + try { + size = headerStream.available(); + } catch (IOException ex) { + // should never occur + } + parse(headerStream); + bodyLoaded = true; + setHeadersLoaded(true); + } else { + // Load the obtained headers. + InternetHeaders h = new InternetHeaders(); + // Some IMAP servers (e.g., gmx.net) return NIL + // instead of a string just containing a CR/LF + // when the header list is empty. + if (headerStream != null) + h.load(headerStream); + if (headers == null || allHeaders) + headers = h; + else { + /* + * This is really painful. A second fetch + * of the same headers (which might occur because + * a new header was added to the set requested) + * will return headers we already know about. + * In this case, only load the headers we haven't + * seen before to avoid adding duplicates of + * headers we already have. + * + * XXX - There's a race condition here if another + * thread is reading headers in the same message + * object, because InternetHeaders is not thread + * safe. + */ + Enumeration
e = h.getAllHeaders(); + while (e.hasMoreElements()) { + Header he = e.nextElement(); + if (!isHeaderLoaded(he.getName())) + headers.addHeader( + he.getName(), he.getValue()); + } + } + + // if we asked for all headers, assume we got them + if (allHeaders) + setHeadersLoaded(true); + else { + // Mark all headers we asked for as 'loaded' + for (int k = 0; k < hdrs.length; k++) + setHeaderLoaded(hdrs[k]); + } + } + } else + return false; // not handled + return true; // something above handled it + } + + /** + * Apply the data in the extension FETCH items to this message. + * This method adds all the items to the items map. + * Subclasses may override this method to call super and then + * also copy the data to a more convenient form. + * + * ASSERT: Must hold the messageCacheLock. + * + * @param extensionItems the Map to add fetch items to + * @since JavaMail 1.4.6 + */ + protected void handleExtensionFetchItems( + Map extensionItems) { + if (extensionItems == null || extensionItems.isEmpty()) + return; + if (items == null) + items = new HashMap<>(); + items.putAll(extensionItems); + } + + /** + * Fetch an individual item for the current message. + * Note that handleExtensionFetchItems will have been called + * to store this item in the message before this method + * returns. + * + * @param fitem the FetchItem + * @return the data associated with the FetchItem + * @exception MessagingException for failures + * @since JavaMail 1.4.6 + */ + protected Object fetchItem(FetchItem fitem) + throws MessagingException { + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + Object robj = null; + + try { + IMAPProtocol p = getProtocol(); + + checkExpunged(); // Insure that this message is not expunged + + int seqnum = getSequenceNumber(); + Response[] r = p.fetch(seqnum, fitem.getName()); + + for (int i = 0; i < r.length; i++) { + // If this response is NOT a FetchResponse or if it does + // not match our seqnum, skip. + if (r[i] == null || + !(r[i] instanceof FetchResponse) || + ((FetchResponse)r[i]).getNumber() != seqnum) + continue; + + FetchResponse f = (FetchResponse)r[i]; + handleExtensionFetchItems(f.getExtensionItems()); + if (items != null) { + Object o = items.get(fitem.getName()); + if (o != null) + robj = o; + } + } + + // ((IMAPFolder)folder).handleResponses(r); + p.notifyResponseHandlers(r); + p.handleResult(r[r.length - 1]); + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + return robj; + + } // Release MessageCacheLock + } + + /** + * Return the data associated with the FetchItem. + * If the data hasn't been fetched, call the fetchItem + * method to fetch it. Returns null if there is no + * data for the FetchItem. + * + * @param fitem the FetchItem + * @return the data associated with the FetchItem + * @exception MessagingException for failures + * @since JavaMail 1.4.6 + */ + public synchronized Object getItem(FetchItem fitem) + throws MessagingException { + Object item = items == null ? null : items.get(fitem.getName()); + if (item == null) + item = fetchItem(fitem); + return item; + } + + /* + * Load the Envelope for this message. + */ + private synchronized void loadEnvelope() throws MessagingException { + if (envelope != null) // already loaded + return; + + Response[] r = null; + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + + checkExpunged(); // Insure that this message is not expunged + + int seqnum = getSequenceNumber(); + r = p.fetch(seqnum, EnvelopeCmd); + + for (int i = 0; i < r.length; i++) { + // If this response is NOT a FetchResponse or if it does + // not match our seqnum, skip. + if (r[i] == null || + !(r[i] instanceof FetchResponse) || + ((FetchResponse)r[i]).getNumber() != seqnum) + continue; + + FetchResponse f = (FetchResponse)r[i]; + + // Look for the Envelope items. + int count = f.getItemCount(); + for (int j = 0; j < count; j++) { + Item item = f.getItem(j); + + if (item instanceof ENVELOPE) + envelope = (ENVELOPE)item; + else if (item instanceof INTERNALDATE) + receivedDate = ((INTERNALDATE)item).getDate(); + else if (item instanceof RFC822SIZE) + size = ((RFC822SIZE)item).size; + } + } + + // ((IMAPFolder)folder).handleResponses(r); + p.notifyResponseHandlers(r); + p.handleResult(r[r.length - 1]); + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + + } // Release MessageCacheLock + + if (envelope == null) + throw new MessagingException("Failed to load IMAP envelope"); + } + + /* + * Load the BODYSTRUCTURE + */ + private synchronized void loadBODYSTRUCTURE() + throws MessagingException { + if (bs != null) // already loaded + return; + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + + // This message could be expunged when we were waiting + // to acquire the lock ... + checkExpunged(); + + bs = p.fetchBodyStructure(getSequenceNumber()); + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + if (bs == null) { + // if the FETCH is successful, we should always get a + // BODYSTRUCTURE, but some servers fail to return it + // if the message has been expunged + forceCheckExpunged(); + throw new MessagingException("Unable to load BODYSTRUCTURE"); + } + } + } + + /* + * Load all headers. + */ + private synchronized void loadHeaders() throws MessagingException { + if (headersLoaded) + return; + + InputStream is = null; + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized (getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + + // This message could be expunged when we were waiting + // to acquire the lock ... + checkExpunged(); + + if (p.isREV1()) { + BODY b = p.peekBody(getSequenceNumber(), + toSection("HEADER")); + if (b != null) + is = b.getByteArrayInputStream(); + } else { + RFC822DATA rd = p.fetchRFC822(getSequenceNumber(), + "HEADER"); + if (rd != null) + is = rd.getByteArrayInputStream(); + } + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + } // Release MessageCacheLock + + if (is == null) + throw new MessagingException("Cannot load header"); + headers = new InternetHeaders(is); + headersLoaded = true; + } + + /* + * Load this message's Flags + */ + private synchronized void loadFlags() throws MessagingException { + if (flags != null) + return; + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + + // This message could be expunged when we were waiting + // to acquire the lock ... + checkExpunged(); + + flags = p.fetchFlags(getSequenceNumber()); + // make sure flags is always set, even if server is broken + if (flags == null) + flags = new Flags(); + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + } // Release MessageCacheLock + } + + /* + * Are all headers loaded? + */ + private boolean areHeadersLoaded() { + return headersLoaded; + } + + /* + * Set whether all headers are loaded. + */ + private void setHeadersLoaded(boolean loaded) { + headersLoaded = loaded; + } + + /* + * Check if the given header was ever loaded from the server + */ + private boolean isHeaderLoaded(String name) { + if (headersLoaded) // All headers for this message have been loaded + return true; + + return loadedHeaders.containsKey(name.toUpperCase(Locale.ENGLISH)); + } + + /* + * Mark that the given headers have been loaded from the server. + */ + private void setHeaderLoaded(String name) { + loadedHeaders.put(name.toUpperCase(Locale.ENGLISH), name); + } + + /* + * Convert the given FETCH item identifier to the approriate + * section-string for this message. + */ + private String toSection(String what) { + if (sectionId == null) + return what; + else + return sectionId + "." + what; + } + + /* + * Clone an array of InternetAddresses. + */ + private InternetAddress[] aaclone(InternetAddress[] aa) { + if (aa == null) + return null; + else + return aa.clone(); + } + + private Flags _getFlags() { + return flags; + } + + private ENVELOPE _getEnvelope() { + return envelope; + } + + private BODYSTRUCTURE _getBodyStructure() { + return bs; + } + + /*********************************************************** + * accessor routines to make available certain private/protected + * fields to other classes in this package. + ***********************************************************/ + + /* + * Called by IMAPFolder. + * Must not be synchronized. + */ + void _setFlags(Flags flags) { + this.flags = flags; + } + + /* + * Called by IMAPNestedMessage. + */ + Session _getSession() { + return session; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IMAPMultipartDataSource.java b/app/src/main/java/com/sun/mail/imap/IMAPMultipartDataSource.java new file mode 100644 index 0000000000..c55ef022c2 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IMAPMultipartDataSource.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + + +import javax.mail.*; +import javax.mail.internet.*; + +import com.sun.mail.imap.protocol.*; +import java.util.ArrayList; +import java.util.List; + +/** + * This class + * + * @author John Mani + */ + +public class IMAPMultipartDataSource extends MimePartDataSource + implements MultipartDataSource { + private List parts; + + protected IMAPMultipartDataSource(MimePart part, BODYSTRUCTURE[] bs, + String sectionId, IMAPMessage msg) { + super(part); + + parts = new ArrayList<>(bs.length); + for (int i = 0; i < bs.length; i++) + parts.add( + new IMAPBodyPart(bs[i], + sectionId == null ? + Integer.toString(i+1) : + sectionId + "." + Integer.toString(i+1), + msg) + ); + } + + @Override + public int getCount() { + return parts.size(); + } + + @Override + public BodyPart getBodyPart(int index) throws MessagingException { + return parts.get(index); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IMAPNestedMessage.java b/app/src/main/java/com/sun/mail/imap/IMAPNestedMessage.java new file mode 100644 index 0000000000..12cd4e453a --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IMAPNestedMessage.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.*; +import javax.mail.*; +import com.sun.mail.imap.protocol.*; +import com.sun.mail.iap.ProtocolException; + +/** + * This class implements a nested IMAP message + * + * @author John Mani + */ + +public class IMAPNestedMessage extends IMAPMessage { + private IMAPMessage msg; // the enclosure of this nested message + + /** + * Package private constructor.

+ * + * Note that nested messages have no containing folder, nor + * a message number. + */ + IMAPNestedMessage(IMAPMessage m, BODYSTRUCTURE b, ENVELOPE e, String sid) { + super(m._getSession()); + msg = m; + bs = b; + envelope = e; + sectionId = sid; + setPeek(m.getPeek()); + } + + /* + * Get the enclosing message's Protocol object. Overrides + * IMAPMessage.getProtocol(). + */ + @Override + protected IMAPProtocol getProtocol() + throws ProtocolException, FolderClosedException { + return msg.getProtocol(); + } + + /* + * Is this an IMAP4 REV1 server? + */ + @Override + protected boolean isREV1() throws FolderClosedException { + return msg.isREV1(); + } + + /* + * Get the enclosing message's messageCacheLock. Overrides + * IMAPMessage.getMessageCacheLock(). + */ + @Override + protected Object getMessageCacheLock() { + return msg.getMessageCacheLock(); + } + + /* + * Get the enclosing message's sequence number. Overrides + * IMAPMessage.getSequenceNumber(). + */ + @Override + protected int getSequenceNumber() { + return msg.getSequenceNumber(); + } + + /* + * Check whether the enclosing message is expunged. Overrides + * IMAPMessage.checkExpunged(). + */ + @Override + protected void checkExpunged() throws MessageRemovedException { + msg.checkExpunged(); + } + + /* + * Check whether the enclosing message is expunged. Overrides + * Message.isExpunged(). + */ + @Override + public boolean isExpunged() { + return msg.isExpunged(); + } + + /* + * Get the enclosing message's fetchBlockSize. + */ + @Override + protected int getFetchBlockSize() { + return msg.getFetchBlockSize(); + } + + /* + * Get the enclosing message's ignoreBodyStructureSize. + */ + @Override + protected boolean ignoreBodyStructureSize() { + return msg.ignoreBodyStructureSize(); + } + + /* + * IMAPMessage uses RFC822.SIZE. We use the "size" field from + * our BODYSTRUCTURE. + */ + @Override + public int getSize() throws MessagingException { + return bs.size; + } + + /* + * Disallow setting flags on nested messages + */ + @Override + public synchronized void setFlags(Flags flag, boolean set) + throws MessagingException { + // Cannot set FLAGS on a nested IMAP message + throw new MethodNotSupportedException( + "Cannot set flags on this nested message"); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IMAPProvider.java b/app/src/main/java/com/sun/mail/imap/IMAPProvider.java new file mode 100644 index 0000000000..0f35374380 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IMAPProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import javax.mail.Provider; + +import com.sun.mail.util.DefaultProvider; + +/** + * The IMAP protocol provider. + */ +@DefaultProvider // Remove this annotation if you copy this provider +public class IMAPProvider extends Provider { + public IMAPProvider() { + super(Provider.Type.STORE, "imap", IMAPStore.class.getName(), + "Oracle", null); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IMAPSSLProvider.java b/app/src/main/java/com/sun/mail/imap/IMAPSSLProvider.java new file mode 100644 index 0000000000..354e63d490 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IMAPSSLProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import javax.mail.Provider; + +import com.sun.mail.util.DefaultProvider; + +/** + * The IMAP SSL protocol provider. + */ +@DefaultProvider // Remove this annotation if you copy this provider +public class IMAPSSLProvider extends Provider { + public IMAPSSLProvider() { + super(Provider.Type.STORE, "imaps", IMAPSSLStore.class.getName(), + "Oracle", null); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IMAPSSLStore.java b/app/src/main/java/com/sun/mail/imap/IMAPSSLStore.java new file mode 100644 index 0000000000..488b4ec13c --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IMAPSSLStore.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import javax.mail.*; + +/** + * This class provides access to an IMAP message store over SSL. + */ + +public class IMAPSSLStore extends IMAPStore { + + /** + * Constructor that takes a Session object and a URLName that + * represents a specific IMAP server. + * + * @param session the Session + * @param url the URLName of this store + */ + public IMAPSSLStore(Session session, URLName url) { + super(session, url, "imaps", true); // call super constructor + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IMAPStore.java b/app/src/main/java/com/sun/mail/imap/IMAPStore.java new file mode 100644 index 0000000000..866f177377 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IMAPStore.java @@ -0,0 +1,2211 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.lang.reflect.*; +import java.util.Vector; +import java.util.StringTokenizer; +import java.util.Locale; +import java.util.Properties; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Map; +import java.util.HashMap; +import java.util.logging.Level; + +import javax.mail.*; +import javax.mail.event.*; + +import com.sun.mail.iap.*; +import com.sun.mail.imap.protocol.*; +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.SocketConnectException; +import com.sun.mail.util.MailConnectException; +import java.util.ArrayList; +import java.util.List; + +/** + * This class provides access to an IMAP message store.

+ * + * Applications that need to make use of IMAP-specific features may cast + * a Store object to an IMAPStore object and + * use the methods on this class. The {@link #getQuota getQuota} and + * {@link #setQuota setQuota} methods support the IMAP QUOTA extension. + * Refer to RFC 2087 + * for more information.

+ * + * The {@link #id id} method supports the IMAP ID extension; + * see RFC 2971. + * The fields ID_NAME, ID_VERSION, etc. represent the suggested field names + * in RFC 2971 section 3.3 and may be used as keys in the Map containing + * client values or server values.

+ * + * See the com.sun.mail.imap package + * documentation for further information on the IMAP protocol provider.

+ * + * WARNING: The APIs unique to this class should be + * considered EXPERIMENTAL. They may be changed in the + * future in ways that are incompatible with applications using the + * current APIs. + * + * @author John Mani + * @author Bill Shannon + * @author Jim Glennon + */ +/* + * This package is implemented over the "imap.protocol" package, which + * implements the protocol-level commands.

+ * + * A connected IMAPStore maintains a pool of IMAP protocol objects for + * use in communicating with the IMAP server. The IMAPStore will create + * the initial AUTHENTICATED connection and seed the pool with this + * connection. As folders are opened and new IMAP protocol objects are + * needed, the IMAPStore will provide them from the connection pool, + * or create them if none are available. When a folder is closed, + * its IMAP protocol object is returned to the connection pool if the + * pool is not over capacity. The pool size can be configured by setting + * the mail.imap.connectionpoolsize property.

+ * + * Note that all connections in the connection pool have their response + * handler set to be the Store. When the connection is removed from the + * pool for use by a folder, the response handler is removed and then set + * to either the Folder or to the special nonStoreResponseHandler, depending + * on how the connection is being used. This is probably excessive. + * Better would be for the Protocol object to support only a single + * response handler, which would be set before the connection is used + * and cleared when the connection is in the pool and can't be used.

+ * + * A mechanism is provided for timing out idle connection pool IMAP + * protocol objects. Timed out connections are closed and removed (pruned) + * from the connection pool. The time out interval can be configured via + * the mail.imap.connectionpooltimeout property.

+ * + * The connected IMAPStore object may or may not maintain a separate IMAP + * protocol object that provides the store a dedicated connection to the + * IMAP server. This is provided mainly for compatibility with previous + * implementations of Jakarta Mail and is determined by the value of the + * mail.imap.separatestoreconnection property.

+ * + * An IMAPStore object provides closed IMAPFolder objects thru its list() + * and listSubscribed() methods. A closed IMAPFolder object acquires an + * IMAP protocol object from the store to communicate with the server. When + * the folder is opened, it gets its own protocol object and thus its own, + * separate connection to the server. The store maintains references to + * all 'open' folders. When a folder is/gets closed, the store removes + * it from its list. When the store is/gets closed, it closes all open + * folders in its list, thus cleaning up all open connections to the + * server.

+ * + * A mutex is used to control access to the connection pool resources. + * Any time any of these resources need to be accessed, the following + * convention should be followed: + * + * synchronized (pool) { // ACQUIRE LOCK + * // access connection pool resources + * } // RELEASE LOCK

+ * + * The locking relationship between the store and folders is that the + * store lock must be acquired before a folder lock. This is currently only + * applicable in the store's cleanup method. It's important that the + * connection pool lock is not held when calling into folder objects. + * The locking hierarchy is that a folder lock must be acquired before + * any connection pool operations are performed. You never need to hold + * all three locks, but if you hold more than one this is the order you + * have to acquire them in.

+ * + * That is: Store > Folder, Folder > pool, Store > pool

+ * + * The IMAPStore implements the ResponseHandler interface and listens to + * BYE or untagged OK-notification events from the server as a result of + * Store operations. IMAPFolder forwards notifications that result from + * Folder operations using the store connection; the IMAPStore ResponseHandler + * is not used directly in this case.

+ */ + +public class IMAPStore extends Store + implements QuotaAwareStore, ResponseHandler { + + /** + * A special event type for a StoreEvent to indicate an IMAP + * response, if the mail.imap.enableimapevents property is set. + */ + public static final int RESPONSE = 1000; + + public static final String ID_NAME = "name"; + public static final String ID_VERSION = "version"; + public static final String ID_OS = "os"; + public static final String ID_OS_VERSION = "os-version"; + public static final String ID_VENDOR = "vendor"; + public static final String ID_SUPPORT_URL = "support-url"; + public static final String ID_ADDRESS = "address"; + public static final String ID_DATE = "date"; + public static final String ID_COMMAND = "command"; + public static final String ID_ARGUMENTS = "arguments"; + public static final String ID_ENVIRONMENT = "environment"; + + protected final String name; // name of this protocol + protected final int defaultPort; // default IMAP port + protected final boolean isSSL; // use SSL? + + private final int blksize; // Block size for data requested + // in FETCH requests. Defaults to + // 16K + + private boolean ignoreSize; // ignore the size in BODYSTRUCTURE? + + private final int statusCacheTimeout; // cache Status for 1 second + + private final int appendBufferSize; // max size of msg buffered for append + + private final int minIdleTime; // minimum idle time + + private volatile int port = -1; // port to use + + // Auth info + protected String host; + protected String user; + protected String password; + protected String proxyAuthUser; + protected String authorizationID; + protected String saslRealm; + + private Namespaces namespaces; + + private boolean enableStartTLS = false; // enable STARTTLS + private boolean requireStartTLS = false; // require STARTTLS + private boolean usingSSL = false; // using SSL? + private boolean enableSASL = false; // enable SASL authentication + private String[] saslMechanisms; + private boolean forcePasswordRefresh = false; + // enable notification of IMAP responses + private boolean enableResponseEvents = false; + // enable notification of IMAP responses during IDLE + private boolean enableImapEvents = false; + private String guid; // for Yahoo! Mail IMAP + private boolean throwSearchException = false; + private boolean peek = false; + private boolean closeFoldersOnStoreFailure = true; + private boolean enableCompress = false; // enable COMPRESS=DEFLATE + private boolean finalizeCleanClose = false; + + /* + * This field is set in the Store's response handler if we see + * a BYE response. The releaseStore method checks this field + * and if set it cleans up the Store. Field is volatile because + * there's no lock we consistently hold while manipulating it. + * + * Because volatile doesn't really work before JDK 1.5, + * use a lock to protect these two fields. + */ + private volatile boolean connectionFailed = false; + private volatile boolean forceClose = false; + private final Object connectionFailedLock = new Object(); + + private boolean debugusername; // include username in debug output? + private boolean debugpassword; // include password in debug output? + protected MailLogger logger; // for debug output + + private boolean messageCacheDebug; + + // constructors for IMAPFolder class provided by user + private volatile Constructor folderConstructor = null; + private volatile Constructor folderConstructorLI = null; + + // Connection pool info + + static class ConnectionPool { + + // container for the pool's IMAP protocol objects + private Vector authenticatedConnections + = new Vector<>(); + + // vectore of open folders + private Vector folders; + + // is the store connection being used? + private boolean storeConnectionInUse = false; + + // the last time (in millis) the pool was checked for timed out + // connections + private long lastTimePruned; + + // flag to indicate whether there is a dedicated connection for + // store commands + private final boolean separateStoreConnection; + + // client timeout interval + private final long clientTimeoutInterval; + + // server timeout interval + private final long serverTimeoutInterval; + + // size of the connection pool + private final int poolSize; + + // interval for checking for timed out connections + private final long pruningInterval; + + // connection pool logger + private final MailLogger logger; + + /* + * The idleState field supports the IDLE command. + * Normally when executing an IMAP command we hold the + * store's lock. + * While executing the IDLE command we can't hold the + * lock or it would prevent other threads from + * entering Store methods even far enough to check whether + * an IDLE command is in progress. We need to check before + * issuing another command so that we can abort the IDLE + * command. + * + * The idleState field is protected by the store's lock. + * The RUNNING state is the normal state and means no IDLE + * command is in progress. The IDLE state means we've issued + * an IDLE command and are reading responses. The ABORTING + * state means we've sent the DONE continuation command and + * are waiting for the thread running the IDLE command to + * break out of its read loop. + * + * When an IDLE command is in progress, the thread calling + * the idle method will be reading from the IMAP connection + * while not holding the store's lock. + * It's obviously critical that no other thread try to send a + * command or read from the connection while in this state. + * However, other threads can send the DONE continuation + * command that will cause the server to break out of the IDLE + * loop and send the ending tag response to the IDLE command. + * The thread in the idle method that's reading the responses + * from the IDLE command will see this ending response and + * complete the idle method, setting the idleState field back + * to RUNNING, and notifying any threads waiting to use the + * connection. + * + * All uses of the IMAP connection (IMAPProtocol object) must + * be preceeded by a check to make sure an IDLE command is not + * running, and abort the IDLE command if necessary. This check + * is made while holding the connection pool lock. While + * waiting for the IDLE command to complete, these other threads + * will give up the connection pool lock. This check is done by + * the getStoreProtocol() method. + */ + private static final int RUNNING = 0; // not doing IDLE command + private static final int IDLE = 1; // IDLE command in effect + private static final int ABORTING = 2; // IDLE command aborting + private int idleState = RUNNING; + private IMAPProtocol idleProtocol; // protocol object when IDLE + + ConnectionPool(String name, MailLogger plogger, Session session) { + lastTimePruned = System.currentTimeMillis(); + Properties props = session.getProperties(); + + boolean debug = PropUtil.getBooleanProperty(props, + "mail." + name + ".connectionpool.debug", false); + logger = plogger.getSubLogger("connectionpool", + "DEBUG IMAP CP", debug); + + // check if the default connection pool size is overridden + int size = PropUtil.getIntProperty(props, + "mail." + name + ".connectionpoolsize", -1); + if (size > 0) { + poolSize = size; + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.connectionpoolsize: " + poolSize); + } else + poolSize = 1; + + // check if the default client-side timeout value is overridden + int connectionPoolTimeout = PropUtil.getIntProperty(props, + "mail." + name + ".connectionpooltimeout", -1); + if (connectionPoolTimeout > 0) { + clientTimeoutInterval = connectionPoolTimeout; + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.connectionpooltimeout: " + + clientTimeoutInterval); + } else + clientTimeoutInterval = 45 * 1000; // 45 seconds + + // check if the default server-side timeout value is overridden + int serverTimeout = PropUtil.getIntProperty(props, + "mail." + name + ".servertimeout", -1); + if (serverTimeout > 0) { + serverTimeoutInterval = serverTimeout; + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.servertimeout: " + + serverTimeoutInterval); + } else + serverTimeoutInterval = 30 * 60 * 1000; // 30 minutes + + // check if the default server-side timeout value is overridden + int pruning = PropUtil.getIntProperty(props, + "mail." + name + ".pruninginterval", -1); + if (pruning > 0) { + pruningInterval = pruning; + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.pruninginterval: " + + pruningInterval); + } else + pruningInterval = 60 * 1000; // 1 minute + + // check to see if we should use a separate (i.e. dedicated) + // store connection + separateStoreConnection = + PropUtil.getBooleanProperty(props, + "mail." + name + ".separatestoreconnection", false); + if (separateStoreConnection) + logger.config("dedicate a store connection"); + + } + } + + private final ConnectionPool pool; + + /** + * A special response handler for connections that are being used + * to perform operations on behalf of an object other than the Store. + * It DOESN'T cause the Store to be cleaned up if a BYE is seen. + * The BYE may be real or synthetic and in either case just indicates + * that the connection is dead. + */ + private ResponseHandler nonStoreResponseHandler = new ResponseHandler() { + @Override + public void handleResponse(Response r) { + // Any of these responses may have a response code. + if (r.isOK() || r.isNO() || r.isBAD() || r.isBYE()) + handleResponseCode(r); + if (r.isBYE()) + logger.fine("IMAPStore non-store connection dead"); + } + }; + + /** + * Constructor that takes a Session object and a URLName that + * represents a specific IMAP server. + * + * @param session the Session + * @param url the URLName of this store + */ + public IMAPStore(Session session, URLName url) { + this(session, url, "imap", false); + } + + /** + * Constructor used by this class and by IMAPSSLStore subclass. + * + * @param session the Session + * @param url the URLName of this store + * @param name the protocol name for this store + * @param isSSL use SSL? + */ + protected IMAPStore(Session session, URLName url, + String name, boolean isSSL) { + super(session, url); // call super constructor + Properties props = session.getProperties(); + + if (url != null) + name = url.getProtocol(); + this.name = name; + if (!isSSL) + isSSL = PropUtil.getBooleanProperty(props, + "mail." + name + ".ssl.enable", false); + if (isSSL) + this.defaultPort = 993; + else + this.defaultPort = 143; + this.isSSL = isSSL; + + debug = session.getDebug(); + debugusername = PropUtil.getBooleanProperty(props, + "mail.debug.auth.username", true); + debugpassword = PropUtil.getBooleanProperty(props, + "mail.debug.auth.password", false); + logger = new MailLogger(this.getClass(), + "DEBUG " + name.toUpperCase(Locale.ENGLISH), + session.getDebug(), session.getDebugOut()); + + boolean partialFetch = PropUtil.getBooleanProperty(props, + "mail." + name + ".partialfetch", true); + if (!partialFetch) { + blksize = -1; + logger.config("mail.imap.partialfetch: false"); + } else { + blksize = PropUtil.getIntProperty(props, + "mail." + name +".fetchsize", 1024 * 16); + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.fetchsize: " + blksize); + } + + ignoreSize = PropUtil.getBooleanProperty(props, + "mail." + name +".ignorebodystructuresize", false); + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.ignorebodystructuresize: " + ignoreSize); + + statusCacheTimeout = PropUtil.getIntProperty(props, + "mail." + name + ".statuscachetimeout", 1000); + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.statuscachetimeout: " + + statusCacheTimeout); + + appendBufferSize = PropUtil.getIntProperty(props, + "mail." + name + ".appendbuffersize", -1); + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.appendbuffersize: " + appendBufferSize); + + minIdleTime = PropUtil.getIntProperty(props, + "mail." + name + ".minidletime", 10); + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.minidletime: " + minIdleTime); + + // check if we should do a PROXYAUTH login + String s = session.getProperty("mail." + name + ".proxyauth.user"); + if (s != null) { + proxyAuthUser = s; + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.proxyauth.user: " + proxyAuthUser); + } + + // check if STARTTLS is enabled + enableStartTLS = PropUtil.getBooleanProperty(props, + "mail." + name + ".starttls.enable", false); + if (enableStartTLS) + logger.config("enable STARTTLS"); + + // check if STARTTLS is required + requireStartTLS = PropUtil.getBooleanProperty(props, + "mail." + name + ".starttls.required", false); + if (requireStartTLS) + logger.config("require STARTTLS"); + + // check if SASL is enabled + enableSASL = PropUtil.getBooleanProperty(props, + "mail." + name + ".sasl.enable", false); + if (enableSASL) + logger.config("enable SASL"); + + // check if SASL mechanisms are specified + if (enableSASL) { + s = session.getProperty("mail." + name + ".sasl.mechanisms"); + if (s != null && s.length() > 0) { + if (logger.isLoggable(Level.CONFIG)) + logger.config("SASL mechanisms allowed: " + s); + List v = new ArrayList<>(5); + StringTokenizer st = new StringTokenizer(s, " ,"); + while (st.hasMoreTokens()) { + String m = st.nextToken(); + if (m.length() > 0) + v.add(m); + } + saslMechanisms = new String[v.size()]; + v.toArray(saslMechanisms); + } + } + + // check if an authorization ID has been specified + s = session.getProperty("mail." + name + ".sasl.authorizationid"); + if (s != null) { + authorizationID = s; + logger.log(Level.CONFIG, "mail.imap.sasl.authorizationid: {0}", + authorizationID); + } + + // check if a SASL realm has been specified + s = session.getProperty("mail." + name + ".sasl.realm"); + if (s != null) { + saslRealm = s; + logger.log(Level.CONFIG, "mail.imap.sasl.realm: {0}", saslRealm); + } + + // check if forcePasswordRefresh is enabled + forcePasswordRefresh = PropUtil.getBooleanProperty(props, + "mail." + name + ".forcepasswordrefresh", false); + if (forcePasswordRefresh) + logger.config("enable forcePasswordRefresh"); + + // check if enableimapevents is enabled + enableResponseEvents = PropUtil.getBooleanProperty(props, + "mail." + name + ".enableresponseevents", false); + if (enableResponseEvents) + logger.config("enable IMAP response events"); + + // check if enableresponseevents is enabled + enableImapEvents = PropUtil.getBooleanProperty(props, + "mail." + name + ".enableimapevents", false); + if (enableImapEvents) + logger.config("enable IMAP IDLE events"); + + // check if message cache debugging set + messageCacheDebug = PropUtil.getBooleanProperty(props, + "mail." + name + ".messagecache.debug", false); + + guid = session.getProperty("mail." + name + ".yahoo.guid"); + if (guid != null) + logger.log(Level.CONFIG, "mail.imap.yahoo.guid: {0}", guid); + + // check if throwsearchexception is enabled + throwSearchException = PropUtil.getBooleanProperty(props, + "mail." + name + ".throwsearchexception", false); + if (throwSearchException) + logger.config("throw SearchException"); + + // check if peek is set + peek = PropUtil.getBooleanProperty(props, + "mail." + name + ".peek", false); + if (peek) + logger.config("peek"); + + // check if closeFoldersOnStoreFailure is set + closeFoldersOnStoreFailure = PropUtil.getBooleanProperty(props, + "mail." + name + ".closefoldersonstorefailure", true); + if (closeFoldersOnStoreFailure) + logger.config("closeFoldersOnStoreFailure"); + + // check if COMPRESS is enabled + enableCompress = PropUtil.getBooleanProperty(props, + "mail." + name + ".compress.enable", false); + if (enableCompress) + logger.config("enable COMPRESS"); + + // check if finalizeCleanClose is enabled + finalizeCleanClose = PropUtil.getBooleanProperty(props, + "mail." + name + ".finalizecleanclose", false); + if (finalizeCleanClose) + logger.config("close connection cleanly in finalize"); + + s = session.getProperty("mail." + name + ".folder.class"); + if (s != null) { + logger.log(Level.CONFIG, "IMAP: folder class: {0}", s); + try { + ClassLoader cl = this.getClass().getClassLoader(); + + // now load the class + Class folderClass = null; + try { + // First try the "application's" class loader. + // This should eventually be replaced by + // Thread.currentThread().getContextClassLoader(). + folderClass = Class.forName(s, false, cl); + } catch (ClassNotFoundException ex1) { + // That didn't work, now try the "system" class loader. + // (Need both of these because JDK 1.1 class loaders + // may not delegate to their parent class loader.) + folderClass = Class.forName(s); + } + + Class[] c = { String.class, char.class, IMAPStore.class, + Boolean.class }; + folderConstructor = folderClass.getConstructor(c); + Class[] c2 = { ListInfo.class, IMAPStore.class }; + folderConstructorLI = folderClass.getConstructor(c2); + } catch (Exception ex) { + logger.log(Level.CONFIG, + "IMAP: failed to load folder class", ex); + } + } + + pool = new ConnectionPool(name, logger, session); + } + + /** + * Implementation of protocolConnect(). Will create a connection + * to the server and authenticate the user using the mechanisms + * specified by various properties.

+ * + * The host, user, and password + * parameters must all be non-null. If the authentication mechanism + * being used does not require a password, an empty string or other + * suitable dummy password should be used. + */ + @Override + protected synchronized boolean + protocolConnect(String host, int pport, String user, String password) + throws MessagingException { + + IMAPProtocol protocol = null; + + // check for non-null values of host, password, user + if (host == null || password == null || user == null) { + if (logger.isLoggable(Level.FINE)) + logger.fine("protocolConnect returning false" + + ", host=" + host + + ", user=" + traceUser(user) + + ", password=" + tracePassword(password)); + return false; + } + + // set the port correctly + if (pport != -1) { + port = pport; + } else { + port = PropUtil.getIntProperty(session.getProperties(), + "mail." + name + ".port", port); + } + + // use the default if needed + if (port == -1) { + port = defaultPort; + } + + try { + boolean poolEmpty; + synchronized (pool) { + poolEmpty = pool.authenticatedConnections.isEmpty(); + } + + if (poolEmpty) { + if (logger.isLoggable(Level.FINE)) + logger.fine("trying to connect to host \"" + host + + "\", port " + port + ", isSSL " + isSSL); + protocol = newIMAPProtocol(host, port); + if (logger.isLoggable(Level.FINE)) + logger.fine("protocolConnect login" + + ", host=" + host + + ", user=" + traceUser(user) + + ", password=" + tracePassword(password)); + protocol.addResponseHandler(nonStoreResponseHandler); + login(protocol, user, password); + protocol.removeResponseHandler(nonStoreResponseHandler); + protocol.addResponseHandler(this); + + usingSSL = protocol.isSSL(); // in case anyone asks + + this.host = host; + this.user = user; + this.password = password; + + synchronized (pool) { + pool.authenticatedConnections.addElement(protocol); + } + } + } catch (IMAPReferralException ex) { + // login failure due to IMAP REFERRAL, close connection to server + if (protocol != null) + protocol.disconnect(); + protocol = null; + throw new ReferralException(ex.getUrl(), ex.getMessage()); + } catch (CommandFailedException cex) { + // login failure, close connection to server + if (protocol != null) + protocol.disconnect(); + protocol = null; + Response r = cex.getResponse(); + throw new AuthenticationFailedException( + r != null ? r.getRest() : cex.getMessage()); + } catch (ProtocolException pex) { // any other exception + // failure in login command, close connection to server + if (protocol != null) + protocol.disconnect(); + protocol = null; + throw new MessagingException(pex.getMessage(), pex); + } catch (SocketConnectException scex) { + throw new MailConnectException(scex); + } catch (IOException ioex) { + throw new MessagingException(ioex.getMessage(), ioex); + } + + return true; + } + + /** + * Create an IMAPProtocol object connected to the host and port. + * Subclasses of IMAPStore may override this method to return a + * subclass of IMAPProtocol that supports product-specific extensions. + * + * @param host the host name + * @param port the port number + * @return the new IMAPProtocol object + * @exception IOException for I/O errors + * @exception ProtocolException for protocol errors + * @since JavaMail 1.4.6 + */ + protected IMAPProtocol newIMAPProtocol(String host, int port) + throws IOException, ProtocolException { + return new IMAPProtocol(name, host, port, + session.getProperties(), + isSSL, + logger + ); + } + + private void login(IMAPProtocol p, String u, String pw) + throws ProtocolException { + // turn on TLS if it's been enabled or required and is supported + // and we're not already using SSL + if ((enableStartTLS || requireStartTLS) && !p.isSSL()) { + if (p.hasCapability("STARTTLS")) { + p.startTLS(); + // if startTLS succeeds, refresh capabilities + p.capability(); + } else if (requireStartTLS) { + logger.fine("STARTTLS required but not supported by server"); + throw new ProtocolException( + "STARTTLS required but not supported by server"); + } + } + if (p.isAuthenticated()) + return; // no need to login + + // allow subclasses to issue commands before login + preLogin(p); + + // issue special ID command to Yahoo! Mail IMAP server + // http://en.wikipedia.org/wiki/Yahoo%21_Mail#Free_IMAP_and_SMTPs_access + if (guid != null) { + Map gmap = new HashMap<>(); + gmap.put("GUID", guid); + p.id(gmap); + } + + /* + * Put a special "marker" in the capabilities list so we can + * detect if the server refreshed the capabilities in the OK + * response. + */ + p.getCapabilities().put("__PRELOGIN__", ""); + String authzid; + if (authorizationID != null) + authzid = authorizationID; + else if (proxyAuthUser != null) + authzid = proxyAuthUser; + else + authzid = null; + + if (enableSASL) { + try { + p.sasllogin(saslMechanisms, saslRealm, authzid, u, pw); + if (!p.isAuthenticated()) + throw new CommandFailedException( + "SASL authentication failed"); + } catch (UnsupportedOperationException ex) { + // continue to try other authentication methods below + } + } + + if (!p.isAuthenticated()) + authenticate(p, authzid, u, pw); + + if (proxyAuthUser != null) + p.proxyauth(proxyAuthUser); + + /* + * If marker is still there, capabilities haven't been refreshed, + * refresh them now. + */ + if (p.hasCapability("__PRELOGIN__")) { + try { + p.capability(); + } catch (ConnectionException cex) { + throw cex; // rethrow connection failures + // XXX - assume connection has been closed + } catch (ProtocolException pex) { + // ignore other exceptions that "should never happen" + } + } + + if (enableCompress) { + if (p.hasCapability("COMPRESS=DEFLATE")) { + p.compress(); + } + } + + // if server supports UTF-8, enable it for client use + // note that this is safe to enable even if mail.mime.allowutf8=false + if (p.hasCapability("UTF8=ACCEPT") || p.hasCapability("UTF8=ONLY")) + p.enable("UTF8=ACCEPT"); + } + + /** + * Authenticate using one of the non-SASL mechanisms. + * + * @param p the IMAPProtocol object + * @param authzid the authorization ID + * @param user the user name + * @param password the password + * @exception ProtocolException on failures + */ + private void authenticate(IMAPProtocol p, String authzid, + String user, String password) + throws ProtocolException { + // this list must match the "if" statements below + String defaultAuthenticationMechanisms = "PLAIN LOGIN NTLM XOAUTH2"; + + // setting mail.imap.auth.mechanisms controls which mechanisms will + // be used, and in what order they'll be considered. only the first + // match is used. + String mechs = session.getProperty("mail." + name + ".auth.mechanisms"); + + if (mechs == null) + mechs = defaultAuthenticationMechanisms; + + /* + * Loop through the list of mechanisms supplied by the user + * (or defaulted) and try each in turn. If the server supports + * the mechanism and we have an authenticator for the mechanism, + * and it hasn't been disabled, use it. + */ + StringTokenizer st = new StringTokenizer(mechs); + while (st.hasMoreTokens()) { + String m = st.nextToken(); + m = m.toUpperCase(Locale.ENGLISH); + + /* + * If using the default mechanisms, check if this one is disabled. + */ + if (mechs == defaultAuthenticationMechanisms) { + String dprop = "mail." + name + ".auth." + + m.toLowerCase(Locale.ENGLISH) + ".disable"; + boolean disabled = PropUtil.getBooleanProperty( + session.getProperties(), + dprop, m.equals("XOAUTH2")); + if (disabled) { + if (logger.isLoggable(Level.FINE)) + logger.fine("mechanism " + m + + " disabled by property: " + dprop); + continue; + } + } + + if (!(p.hasCapability("AUTH=" + m) || + (m.equals("LOGIN") && p.hasCapability("AUTH-LOGIN")))) { + logger.log(Level.FINE, "mechanism {0} not supported by server", + m); + continue; + } + + if (m.equals("PLAIN")) + p.authplain(authzid, user, password); + else if (m.equals("LOGIN")) + p.authlogin(user, password); + else if (m.equals("NTLM")) + p.authntlm(authzid, user, password); + else if (m.equals("XOAUTH2")) + p.authoauth2(user, password); + else { + logger.log(Level.FINE, "no authenticator for mechanism {0}", m); + continue; + } + return; + } + + if (!p.hasCapability("LOGINDISABLED")) { + p.login(user, password); + return; + } + + throw new ProtocolException("No login methods supported!"); + } + + /** + * This method is called after the connection is made and + * TLS is started (if needed), but before any authentication + * is attempted. Subclasses can override this method to + * issue commands that are needed in the "not authenticated" + * state. Note that if the connection is pre-authenticated, + * this method won't be called.

+ * + * The implementation of this method in this class does nothing. + * + * @param p the IMAPProtocol connection + * @exception ProtocolException for protocol errors + * @since JavaMail 1.4.4 + */ + protected void preLogin(IMAPProtocol p) throws ProtocolException { + } + + /** + * Does this IMAPStore use SSL when connecting to the server? + * + * @return true if using SSL + * @since JavaMail 1.4.6 + */ + public synchronized boolean isSSL() { + return usingSSL; + } + + /** + * Set the user name that will be used for subsequent connections + * after this Store is first connected (for example, when creating + * a connection to open a Folder). This value is overridden + * by any call to the Store's connect method.

+ * + * Some IMAP servers may provide an authentication ID that can + * be used for more efficient authentication for future connections. + * This authentication ID is provided in a server-specific manner + * not described here.

+ * + * Most applications will never need to use this method. + * + * @param user the user name for the store + * @since JavaMail 1.3.3 + */ + public synchronized void setUsername(String user) { + this.user = user; + } + + /** + * Set the password that will be used for subsequent connections + * after this Store is first connected (for example, when creating + * a connection to open a Folder). This value is overridden + * by any call to the Store's connect method.

+ * + * Most applications will never need to use this method. + * + * @param password the password for the store + * @since JavaMail 1.3.3 + */ + public synchronized void setPassword(String password) { + this.password = password; + } + + /* + * Get a new authenticated protocol object for this Folder. + * Also store a reference to this folder in our list of + * open folders. + */ + IMAPProtocol getProtocol(IMAPFolder folder) + throws MessagingException { + IMAPProtocol p = null; + + // keep looking for a connection until we get a good one + while (p == null) { + + // New authenticated protocol objects are either acquired + // from the connection pool, or created when the pool is + // empty or no connections are available. None are available + // if the current pool size is one and the separate store + // property is set or the connection is in use. + + synchronized (pool) { + + // If there's none available in the pool, + // create a new one. + if (pool.authenticatedConnections.isEmpty() || + (pool.authenticatedConnections.size() == 1 && + (pool.separateStoreConnection || pool.storeConnectionInUse))) { + + logger.fine("no connections in the pool, creating a new one"); + try { + if (forcePasswordRefresh) + refreshPassword(); + // Use cached host, port and timeout values. + p = newIMAPProtocol(host, port); + p.addResponseHandler(nonStoreResponseHandler); + // Use cached auth info + login(p, user, password); + p.removeResponseHandler(nonStoreResponseHandler); + } catch(Exception ex1) { + if (p != null) + try { + p.disconnect(); + } catch (Exception ex2) { } + p = null; + } + + if (p == null) + throw new MessagingException("connection failure"); + } else { + if (logger.isLoggable(Level.FINE)) + logger.fine("connection available -- size: " + + pool.authenticatedConnections.size()); + + // remove the available connection from the Authenticated queue + p = pool.authenticatedConnections.lastElement(); + pool.authenticatedConnections.removeElement(p); + + // check if the connection is still live + long lastUsed = System.currentTimeMillis() - p.getTimestamp(); + if (lastUsed > pool.serverTimeoutInterval) { + try { + /* + * Swap in a special response handler that will handle + * alerts, but won't cause the store to be closed and + * cleaned up if the connection is dead. + */ + p.removeResponseHandler(this); + p.addResponseHandler(nonStoreResponseHandler); + p.noop(); + p.removeResponseHandler(nonStoreResponseHandler); + p.addResponseHandler(this); + } catch (ProtocolException pex) { + try { + p.removeResponseHandler(nonStoreResponseHandler); + p.disconnect(); + } catch (RuntimeException ignored) { + // don't let any exception stop us + } + p = null; + continue; // try again, from the top + } + } + + // if proxyAuthUser has changed, switch to new user + if (proxyAuthUser != null && + !proxyAuthUser.equals(p.getProxyAuthUser()) && + p.hasCapability("X-UNAUTHENTICATE")) { + try { + /* + * Swap in a special response handler that will handle + * alerts, but won't cause the store to be closed and + * cleaned up if the connection is dead. + */ + p.removeResponseHandler(this); + p.addResponseHandler(nonStoreResponseHandler); + p.unauthenticate(); + login(p, user, password); + p.removeResponseHandler(nonStoreResponseHandler); + p.addResponseHandler(this); + } catch (ProtocolException pex) { + try { + p.removeResponseHandler(nonStoreResponseHandler); + p.disconnect(); + } catch (RuntimeException ignored) { + // don't let any exception stop us + } + p = null; + continue; // try again, from the top + } + } + + // remove the store as a response handler. + p.removeResponseHandler(this); + } + + // check if we need to look for client-side timeouts + timeoutConnections(); + + // Add folder to folder-list + if (folder != null) { + if (pool.folders == null) + pool.folders = new Vector<>(); + pool.folders.addElement(folder); + } + } + + } + + return p; + } + + /** + * Get this Store's protocol connection. + * + * When acquiring a store protocol object, it is important to + * use the following steps: + * + * IMAPProtocol p = null; + * try { + * p = getStoreProtocol(); + * // perform the command + * } catch (ConnectionException cex) { + * throw new StoreClosedException(this, cex.getMessage()); + * } catch (WhateverException ex) { + * // handle it + * } finally { + * releaseStoreProtocol(p); + * } + */ + private IMAPProtocol getStoreProtocol() throws ProtocolException { + IMAPProtocol p = null; + + while (p == null) { + synchronized (pool) { + waitIfIdle(); + + // If there's no authenticated connections available create a + // new one and place it in the authenticated queue. + if (pool.authenticatedConnections.isEmpty()) { + pool.logger.fine("getStoreProtocol() - no connections " + + "in the pool, creating a new one"); + try { + if (forcePasswordRefresh) + refreshPassword(); + // Use cached host, port and timeout values. + p = newIMAPProtocol(host, port); + // Use cached auth info + login(p, user, password); + } catch(Exception ex1) { + if (p != null) + try { + p.logout(); + } catch (Exception ex2) { } + p = null; + } + + if (p == null) + throw new ConnectionException( + "failed to create new store connection"); + + p.addResponseHandler(this); + pool.authenticatedConnections.addElement(p); + + } else { + // Always use the first element in the Authenticated queue. + if (pool.logger.isLoggable(Level.FINE)) + pool.logger.fine("getStoreProtocol() - " + + "connection available -- size: " + + pool.authenticatedConnections.size()); + p = pool.authenticatedConnections.firstElement(); + + // if proxyAuthUser has changed, switch to new user + if (proxyAuthUser != null && + !proxyAuthUser.equals(p.getProxyAuthUser()) && + p.hasCapability("X-UNAUTHENTICATE")) { + p.unauthenticate(); + login(p, user, password); + } + } + + if (pool.storeConnectionInUse) { + try { + // someone else is using the connection, give up + // and wait until they're done + p = null; + pool.wait(); + } catch (InterruptedException ex) { + // restore the interrupted state, which callers might + // depend on + Thread.currentThread().interrupt(); + // don't keep looking for a connection if we've been + // interrupted + throw new ProtocolException( + "Interrupted getStoreProtocol", ex); + } + } else { + pool.storeConnectionInUse = true; + + pool.logger.fine("getStoreProtocol() -- storeConnectionInUse"); + } + + timeoutConnections(); + } + } + return p; + } + + /** + * Get a store protocol object for use by a folder. + */ + IMAPProtocol getFolderStoreProtocol() throws ProtocolException { + IMAPProtocol p = getStoreProtocol(); + p.removeResponseHandler(this); + p.addResponseHandler(nonStoreResponseHandler); + return p; + } + + /* + * Some authentication systems use one time passwords + * or tokens, so each authentication request requires + * a new password. This "kludge" allows a callback + * to application code to get a new password. + * + * XXX - remove this when SASL support is added + */ + private void refreshPassword() { + if (logger.isLoggable(Level.FINE)) + logger.fine("refresh password, user: " + traceUser(user)); + InetAddress addr; + try { + addr = InetAddress.getByName(host); + } catch (UnknownHostException e) { + addr = null; + } + PasswordAuthentication pa = + session.requestPasswordAuthentication(addr, port, + name, null, user); + if (pa != null) { + user = pa.getUserName(); + password = pa.getPassword(); + } + } + + /** + * If a SELECT succeeds, but indicates that the folder is + * READ-ONLY, and the user asked to open the folder READ_WRITE, + * do we allow the open to succeed? + */ + boolean allowReadOnlySelect() { + return PropUtil.getBooleanProperty(session.getProperties(), + "mail." + name + ".allowreadonlyselect", false); + } + + /** + * Report whether the separateStoreConnection is set. + */ + boolean hasSeparateStoreConnection() { + return pool.separateStoreConnection; + } + + /** + * Return the connection pool logger. + */ + MailLogger getConnectionPoolLogger() { + return pool.logger; + } + + /** + * Report whether message cache debugging is enabled. + */ + boolean getMessageCacheDebug() { + return messageCacheDebug; + } + + /** + * Report whether the connection pool is full. + */ + boolean isConnectionPoolFull() { + + synchronized (pool) { + if (pool.logger.isLoggable(Level.FINE)) + pool.logger.fine("connection pool current size: " + + pool.authenticatedConnections.size() + + " pool size: " + pool.poolSize); + + return (pool.authenticatedConnections.size() >= pool.poolSize); + + } + } + + /** + * Release the protocol object back to the connection pool. + */ + void releaseProtocol(IMAPFolder folder, IMAPProtocol protocol) { + + synchronized (pool) { + if (protocol != null) { + // If the pool is not full, add the store as a response handler + // and return the protocol object to the connection pool. + if (!isConnectionPoolFull()) { + protocol.addResponseHandler(this); + pool.authenticatedConnections.addElement(protocol); + + if (logger.isLoggable(Level.FINE)) + logger.fine( + "added an Authenticated connection -- size: " + + pool.authenticatedConnections.size()); + } else { + logger.fine( + "pool is full, not adding an Authenticated connection"); + try { + protocol.logout(); + } catch (ProtocolException pex) {}; + } + } + + if (pool.folders != null) + pool.folders.removeElement(folder); + + timeoutConnections(); + } + } + + /** + * Release the store connection. + */ + private void releaseStoreProtocol(IMAPProtocol protocol) { + + // will be called from idle() without the Store lock held, + // but cleanup is synchronized and will acquire the Store lock + + if (protocol == null) { + cleanup(); // failed to ever get the connection + return; // nothing to release + } + + /* + * Read out the flag that says whether this connection failed + * before releasing the protocol object for others to use. + */ + boolean failed; + synchronized (connectionFailedLock) { + failed = connectionFailed; + connectionFailed = false; // reset for next use + } + + // now free the store connection + synchronized (pool) { + pool.storeConnectionInUse = false; + pool.notifyAll(); // in case anyone waiting + + pool.logger.fine("releaseStoreProtocol()"); + + timeoutConnections(); + } + + /* + * If the connection died while we were using it, clean up. + * It's critical that the store connection be freed and the + * connection pool not be locked while we do this. + */ + assert !Thread.holdsLock(pool); + if (failed) + cleanup(); + } + + /** + * Release a store protocol object that was being used by a folder. + */ + void releaseFolderStoreProtocol(IMAPProtocol protocol) { + if (protocol == null) + return; // should never happen + protocol.removeResponseHandler(nonStoreResponseHandler); + protocol.addResponseHandler(this); + synchronized (pool) { + pool.storeConnectionInUse = false; + pool.notifyAll(); // in case anyone waiting + + pool.logger.fine("releaseFolderStoreProtocol()"); + + timeoutConnections(); + } + } + + /** + * Empty the connection pool. + */ + private void emptyConnectionPool(boolean force) { + + synchronized (pool) { + for (int index = pool.authenticatedConnections.size() - 1; + index >= 0; --index) { + try { + IMAPProtocol p = + pool.authenticatedConnections.elementAt(index); + p.removeResponseHandler(this); + if (force) + p.disconnect(); + else + p.logout(); + } catch (ProtocolException pex) {}; + } + + pool.authenticatedConnections.removeAllElements(); + } + + pool.logger.fine("removed all authenticated connections from pool"); + } + + /** + * Check to see if it's time to shrink the connection pool. + */ + private void timeoutConnections() { + + synchronized (pool) { + + // If we've exceeded the pruning interval, look for stale + // connections to logout. + if (System.currentTimeMillis() - pool.lastTimePruned > + pool.pruningInterval && + pool.authenticatedConnections.size() > 1) { + + if (pool.logger.isLoggable(Level.FINE)) { + pool.logger.fine("checking for connections to prune: " + + (System.currentTimeMillis() - pool.lastTimePruned)); + pool.logger.fine("clientTimeoutInterval: " + + pool.clientTimeoutInterval); + } + + IMAPProtocol p; + + // Check the timestamp of the protocol objects in the pool and + // logout if the interval exceeds the client timeout value + // (leave the first connection). + for (int index = pool.authenticatedConnections.size() - 1; + index > 0; index--) { + p = pool.authenticatedConnections. + elementAt(index); + if (pool.logger.isLoggable(Level.FINE)) + pool.logger.fine("protocol last used: " + + (System.currentTimeMillis() - p.getTimestamp())); + if (System.currentTimeMillis() - p.getTimestamp() > + pool.clientTimeoutInterval) { + + pool.logger.fine( + "authenticated connection timed out, " + + "logging out the connection"); + + p.removeResponseHandler(this); + pool.authenticatedConnections.removeElementAt(index); + + try { + p.logout(); + } catch (ProtocolException pex) {} + } + } + pool.lastTimePruned = System.currentTimeMillis(); + } + } + } + + /** + * Get the block size to use for fetch requests on this Store. + */ + int getFetchBlockSize() { + return blksize; + } + + /** + * Ignore the size reported in the BODYSTRUCTURE when fetching data? + */ + boolean ignoreBodyStructureSize() { + return ignoreSize; + } + + /** + * Get a reference to the session. + */ + Session getSession() { + return session; + } + + /** + * Get the number of milliseconds to cache STATUS response. + */ + int getStatusCacheTimeout() { + return statusCacheTimeout; + } + + /** + * Get the maximum size of a message to buffer for append. + */ + int getAppendBufferSize() { + return appendBufferSize; + } + + /** + * Get the minimum amount of time to delay when returning from idle. + */ + int getMinIdleTime() { + return minIdleTime; + } + + /** + * Throw a SearchException if the search expression is too complex? + */ + boolean throwSearchException() { + return throwSearchException; + } + + /** + * Get the default "peek" value. + */ + boolean getPeek() { + return peek; + } + + /** + * Return true if the specified capability string is in the list + * of capabilities the server announced. + * + * @param capability the capability string + * @return true if the server supports this capability + * @exception MessagingException for failures + * @since JavaMail 1.3.3 + */ + public synchronized boolean hasCapability(String capability) + throws MessagingException { + IMAPProtocol p = null; + try { + p = getStoreProtocol(); + return p.hasCapability(capability); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + } + + /** + * Set the user name to be used with the PROXYAUTH command. + * The PROXYAUTH user name can also be set using the + * mail.imap.proxyauth.user property when this + * Store is created. + * + * @param user the user name to set + * @since JavaMail 1.5.1 + */ + public void setProxyAuthUser(String user) { + proxyAuthUser = user; + } + + /** + * Get the user name to be used with the PROXYAUTH command. + * + * @return the user name + * @since JavaMail 1.5.1 + */ + public String getProxyAuthUser() { + return proxyAuthUser; + } + + /** + * Check whether this store is connected. Override superclass + * method, to actually ping our server connection. + */ + @Override + public synchronized boolean isConnected() { + if (!super.isConnected()) { + // if we haven't been connected at all, don't bother with + // the NOOP. + return false; + } + + /* + * The below noop() request can: + * (1) succeed - in which case all is fine. + * + * (2) fail because the server returns NO or BAD, in which + * case we ignore it since we can't really do anything. + * (2) fail because a BYE response is obtained from the + * server + * (3) fail because the socket.write() to the server fails, + * in which case the iap.protocol() code converts the + * IOException into a BYE response. + * + * Thus, our BYE handler will take care of closing the Store + * in case our connection is really gone. + */ + + IMAPProtocol p = null; + try { + p = getStoreProtocol(); + p.noop(); + } catch (ProtocolException pex) { + // will return false below + } finally { + releaseStoreProtocol(p); + } + + + return super.isConnected(); + } + + /** + * Close this Store. + */ + @Override + public synchronized void close() throws MessagingException { + cleanup(); + // do these again in case cleanup returned early + // because we were already closed due to a failure, + // in which case we force close everything + closeAllFolders(true); + emptyConnectionPool(true); + } + + @Override + protected void finalize() throws Throwable { + if (!finalizeCleanClose) { + // when finalizing, close connections abruptly + synchronized (connectionFailedLock) { + connectionFailed = true; + forceClose = true; + } + closeFoldersOnStoreFailure = true; // make sure folders get closed + } + try { + close(); + } finally { + super.finalize(); + } + } + + /** + * Cleanup before dying. + */ + private synchronized void cleanup() { + // if we're not connected, someone beat us to it + if (!super.isConnected()) { + logger.fine("IMAPStore cleanup, not connected"); + return; + } + + /* + * If forceClose is true, some thread ran into an error that suggests + * the server might be dead, so we force the folders to close + * abruptly without waiting for the server. Used when + * the store connection times out, for example. + */ + boolean force; + synchronized (connectionFailedLock) { + force = forceClose; + forceClose = false; + connectionFailed = false; + } + if (logger.isLoggable(Level.FINE)) + logger.fine("IMAPStore cleanup, force " + force); + + if (!force || closeFoldersOnStoreFailure) { + closeAllFolders(force); + } + + emptyConnectionPool(force); + + // to set the state and send the closed connection event + try { + super.close(); + } catch (MessagingException mex) { + // ignore it + } + logger.fine("IMAPStore cleanup done"); + } + + /** + * Close all open Folders. If force is true, close them forcibly. + */ + private void closeAllFolders(boolean force) { + List foldersCopy = null; + boolean done = true; + + // To avoid violating the locking hierarchy, there's no lock we + // can hold that prevents another thread from trying to open a + // folder at the same time we're trying to close all the folders. + // Thus, there's an inherent race condition here. We close all + // the folders we know about and then check whether any new folders + // have been opened in the mean time. We keep trying until we're + // successful in closing all the folders. + for (;;) { + // Make a copy of the folders list so we do not violate the + // folder-connection pool locking hierarchy. + synchronized (pool) { + if (pool.folders != null) { + done = false; + foldersCopy = pool.folders; + pool.folders = null; + } else { + done = true; + } + } + if (done) + break; + + // Close and remove any open folders under this Store. + for (int i = 0, fsize = foldersCopy.size(); i < fsize; i++) { + IMAPFolder f = foldersCopy.get(i); + + try { + if (force) { + logger.fine("force folder to close"); + // Don't want to wait for folder connection to timeout + // (if, for example, the server is down) so we close + // folders abruptly. + f.forceClose(); + } else { + logger.fine("close folder"); + f.close(false); + } + } catch (MessagingException mex) { + // Who cares ?! Ignore 'em. + } catch (IllegalStateException ex) { + // Ditto + } + } + + } + } + + /** + * Get the default folder, representing the root of this user's + * namespace. Returns a closed DefaultFolder object. + */ + @Override + public synchronized Folder getDefaultFolder() throws MessagingException { + checkConnected(); + return new DefaultFolder(this); + } + + /** + * Get named folder. Returns a new, closed IMAPFolder. + */ + @Override + public synchronized Folder getFolder(String name) + throws MessagingException { + checkConnected(); + return newIMAPFolder(name, IMAPFolder.UNKNOWN_SEPARATOR); + } + + /** + * Get named folder. Returns a new, closed IMAPFolder. + */ + @Override + public synchronized Folder getFolder(URLName url) + throws MessagingException { + checkConnected(); + return newIMAPFolder(url.getFile(), IMAPFolder.UNKNOWN_SEPARATOR); + } + + /** + * Create an IMAPFolder object. If user supplied their own class, + * use it. Otherwise, call the constructor. + * + * @param fullName the full name of the folder + * @param separator the separator character for the folder hierarchy + * @param isNamespace does this name represent a namespace? + * @return the new IMAPFolder object + */ + protected IMAPFolder newIMAPFolder(String fullName, char separator, + Boolean isNamespace) { + IMAPFolder f = null; + if (folderConstructor != null) { + try { + Object[] o = + { fullName, Character.valueOf(separator), this, isNamespace }; + f = (IMAPFolder)folderConstructor.newInstance(o); + } catch (Exception ex) { + logger.log(Level.FINE, + "exception creating IMAPFolder class", ex); + } + } + if (f == null) + f = new IMAPFolder(fullName, separator, this, isNamespace); + return f; + } + + /** + * Create an IMAPFolder object. Call the newIMAPFolder method + * above with a null isNamespace. + * + * @param fullName the full name of the folder + * @param separator the separator character for the folder hierarchy + * @return the new IMAPFolder object + */ + protected IMAPFolder newIMAPFolder(String fullName, char separator) { + return newIMAPFolder(fullName, separator, null); + } + + /** + * Create an IMAPFolder object. If user supplied their own class, + * use it. Otherwise, call the constructor. + * + * @param li the ListInfo for the folder + * @return the new IMAPFolder object + */ + protected IMAPFolder newIMAPFolder(ListInfo li) { + IMAPFolder f = null; + if (folderConstructorLI != null) { + try { + Object[] o = { li, this }; + f = (IMAPFolder)folderConstructorLI.newInstance(o); + } catch (Exception ex) { + logger.log(Level.FINE, + "exception creating IMAPFolder class LI", ex); + } + } + if (f == null) + f = new IMAPFolder(li, this); + return f; + } + + /** + * Using the IMAP NAMESPACE command (RFC 2342), return a set + * of folders representing the Personal namespaces. + */ + @Override + public Folder[] getPersonalNamespaces() throws MessagingException { + Namespaces ns = getNamespaces(); + if (ns == null || ns.personal == null) + return super.getPersonalNamespaces(); + return namespaceToFolders(ns.personal, null); + } + + /** + * Using the IMAP NAMESPACE command (RFC 2342), return a set + * of folders representing the User's namespaces. + */ + @Override + public Folder[] getUserNamespaces(String user) + throws MessagingException { + Namespaces ns = getNamespaces(); + if (ns == null || ns.otherUsers == null) + return super.getUserNamespaces(user); + return namespaceToFolders(ns.otherUsers, user); + } + + /** + * Using the IMAP NAMESPACE command (RFC 2342), return a set + * of folders representing the Shared namespaces. + */ + @Override + public Folder[] getSharedNamespaces() throws MessagingException { + Namespaces ns = getNamespaces(); + if (ns == null || ns.shared == null) + return super.getSharedNamespaces(); + return namespaceToFolders(ns.shared, null); + } + + private synchronized Namespaces getNamespaces() throws MessagingException { + checkConnected(); + + IMAPProtocol p = null; + + if (namespaces == null) { + try { + p = getStoreProtocol(); + namespaces = p.namespace(); + } catch (BadCommandException bex) { + // NAMESPACE not supported, ignore it + } catch (ConnectionException cex) { + throw new StoreClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + } + return namespaces; + } + + private Folder[] namespaceToFolders(Namespaces.Namespace[] ns, + String user) { + Folder[] fa = new Folder[ns.length]; + for (int i = 0; i < fa.length; i++) { + String name = ns[i].prefix; + if (user == null) { + // strip trailing delimiter + int len = name.length(); + if ( len > 0 && name.charAt(len - 1) == ns[i].delimiter) + name = name.substring(0, len - 1); + } else { + // add user + name += user; + } + fa[i] = newIMAPFolder(name, ns[i].delimiter, + Boolean.valueOf(user == null)); + } + return fa; + } + + /** + * Get the quotas for the named quota root. + * Quotas are controlled on the basis of a quota root, not + * (necessarily) a folder. The relationship between folders + * and quota roots depends on the IMAP server. Some servers + * might implement a single quota root for all folders owned by + * a user. Other servers might implement a separate quota root + * for each folder. A single folder can even have multiple + * quota roots, perhaps controlling quotas for different + * resources. + * + * @param root the name of the quota root + * @return array of Quota objects + * @exception MessagingException if the server doesn't support the + * QUOTA extension + */ + @Override + public synchronized Quota[] getQuota(String root) + throws MessagingException { + checkConnected(); + Quota[] qa = null; + + IMAPProtocol p = null; + try { + p = getStoreProtocol(); + qa = p.getQuotaRoot(root); + } catch (BadCommandException bex) { + throw new MessagingException("QUOTA not supported", bex); + } catch (ConnectionException cex) { + throw new StoreClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + return qa; + } + + /** + * Set the quotas for the quota root specified in the quota argument. + * Typically this will be one of the quota roots obtained from the + * getQuota method, but it need not be. + * + * @param quota the quota to set + * @exception MessagingException if the server doesn't support the + * QUOTA extension + */ + @Override + public synchronized void setQuota(Quota quota) throws MessagingException { + checkConnected(); + IMAPProtocol p = null; + try { + p = getStoreProtocol(); + p.setQuota(quota); + } catch (BadCommandException bex) { + throw new MessagingException("QUOTA not supported", bex); + } catch (ConnectionException cex) { + throw new StoreClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + } + + private void checkConnected() { + assert Thread.holdsLock(this); + if (!super.isConnected()) + throw new IllegalStateException("Not connected"); + } + + /** + * Response handler method. + */ + @Override + public void handleResponse(Response r) { + // Any of these responses may have a response code. + if (r.isOK() || r.isNO() || r.isBAD() || r.isBYE()) + handleResponseCode(r); + if (r.isBYE()) { + logger.fine("IMAPStore connection dead"); + // Store's IMAP connection is dead, save the response so that + // releaseStoreProtocol will cleanup later. + synchronized (connectionFailedLock) { + connectionFailed = true; + if (r.isSynthetic()) + forceClose = true; + } + return; + } + } + + /** + * Use the IMAP IDLE command (see + * RFC 2177), + * if supported by the server, to enter idle mode so that the server + * can send unsolicited notifications + * without the need for the client to constantly poll the server. + * Use a ConnectionListener to be notified of + * events. When another thread (e.g., the listener thread) + * needs to issue an IMAP comand for this Store, the idle mode will + * be terminated and this method will return. Typically the caller + * will invoke this method in a loop.

+ * + * If the mail.imap.enableimapevents property is set, notifications + * received while the IDLE command is active will be delivered to + * ConnectionListeners as events with a type of + * IMAPStore.RESPONSE. The event's message will be + * the raw IMAP response string. + * Note that most IMAP servers will not deliver any events when + * using the IDLE command on a connection with no mailbox selected + * (i.e., this method). In most cases you'll want to use the + * idle method on IMAPFolder.

+ * + * NOTE: This capability is highly experimental and likely will change + * in future releases.

+ * + * The mail.imap.minidletime property enforces a minimum delay + * before returning from this method, to ensure that other threads + * have a chance to issue commands before the caller invokes this + * method again. The default delay is 10 milliseconds. + * + * @exception MessagingException if the server doesn't support the + * IDLE extension + * @exception IllegalStateException if the store isn't connected + * + * @since JavaMail 1.4.1 + */ + public void idle() throws MessagingException { + IMAPProtocol p = null; + // ASSERT: Must NOT be called with the connection pool + // synchronization lock held. + assert !Thread.holdsLock(pool); + synchronized (this) { + checkConnected(); + } + boolean needNotification = false; + try { + synchronized (pool) { + p = getStoreProtocol(); + if (pool.idleState != ConnectionPool.RUNNING) { + // some other thread must be running the IDLE + // command, we'll just wait for it to finish + // without aborting it ourselves + try { + // give up lock and wait to be not idle + pool.wait(); + } catch (InterruptedException ex) { + // restore the interrupted state, which callers might + // depend on + Thread.currentThread().interrupt(); + // stop waiting and return to caller + throw new MessagingException("idle interrupted", ex); + } + return; + } + p.idleStart(); + needNotification = true; + pool.idleState = ConnectionPool.IDLE; + pool.idleProtocol = p; + } + + /* + * We gave up the pool lock so that other threads + * can get into the pool far enough to see that we're + * in IDLE and abort the IDLE. + * + * Now we read responses from the IDLE command, especially + * including unsolicited notifications from the server. + * We don't hold the pool lock while reading because + * it protects the idleState and other threads need to be + * able to examine the state. + * + * We hold the pool lock while processing the responses. + */ + for (;;) { + Response r = p.readIdleResponse(); + synchronized (pool) { + if (r == null || !p.processIdleResponse(r)) { + pool.idleState = ConnectionPool.RUNNING; + pool.idleProtocol = null; + pool.notifyAll(); + needNotification = false; + break; + } + } + if (enableImapEvents && r.isUnTagged()) { + notifyStoreListeners(IMAPStore.RESPONSE, r.toString()); + } + } + + /* + * Enforce a minimum delay to give time to threads + * processing the responses that came in while we + * were idle. + */ + int minidle = getMinIdleTime(); + if (minidle > 0) { + try { + Thread.sleep(minidle); + } catch (InterruptedException ex) { + // restore the interrupted state, which callers might + // depend on + Thread.currentThread().interrupt(); + } + } + + } catch (BadCommandException bex) { + throw new MessagingException("IDLE not supported", bex); + } catch (ConnectionException cex) { + throw new StoreClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + if (needNotification) { + synchronized (pool) { + pool.idleState = ConnectionPool.RUNNING; + pool.idleProtocol = null; + pool.notifyAll(); + } + } + releaseStoreProtocol(p); + } + } + + /* + * If an IDLE command is in progress, abort it if necessary, + * and wait until it completes. + * ASSERT: Must be called with the pool's lock held. + */ + private void waitIfIdle() throws ProtocolException { + assert Thread.holdsLock(pool); + while (pool.idleState != ConnectionPool.RUNNING) { + if (pool.idleState == ConnectionPool.IDLE) { + pool.idleProtocol.idleAbort(); + pool.idleState = ConnectionPool.ABORTING; + } + try { + // give up lock and wait to be not idle + pool.wait(); + } catch (InterruptedException ex) { + // If someone is trying to interrupt us we can't keep going + // around the loop waiting for IDLE to complete, but we can't + // just return because callers expect the idleState to be + // RUNNING when we return. Throwing this exception seems + // like the best choice. + throw new ProtocolException("Interrupted waitIfIdle", ex); + } + } + } + + /** + * Send the IMAP ID command (if supported by the server) and return + * the result from the server. The ID command identfies the client + * to the server and returns information about the server to the client. + * See RFC 2971. + * The returned Map is unmodifiable. + * + * @param clientParams a Map of keys and values identifying the client + * @return a Map of keys and values identifying the server + * @exception MessagingException if the server doesn't support the + * ID extension + * @since JavaMail 1.5.1 + */ + public synchronized Map id(Map clientParams) + throws MessagingException { + checkConnected(); + Map serverParams = null; + + IMAPProtocol p = null; + try { + p = getStoreProtocol(); + serverParams = p.id(clientParams); + } catch (BadCommandException bex) { + throw new MessagingException("ID not supported", bex); + } catch (ConnectionException cex) { + throw new StoreClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + return serverParams; + } + + /** + * Handle notifications and alerts. + * Response must be an OK, NO, BAD, or BYE response. + */ + void handleResponseCode(Response r) { + if (enableResponseEvents) + notifyStoreListeners(IMAPStore.RESPONSE, r.toString()); + String s = r.getRest(); // get the text after the response + boolean isAlert = false; + if (s.startsWith("[")) { // a response code + int i = s.indexOf(']'); + // remember if it's an alert + if (i > 0 && s.substring(0, i + 1).equalsIgnoreCase("[ALERT]")) + isAlert = true; + // strip off the response code in any event + s = s.substring(i + 1).trim(); + } + if (isAlert) + notifyStoreListeners(StoreEvent.ALERT, s); + else if (r.isUnTagged() && s.length() > 0) + // Only send notifications that come with untagged + // responses, and only if there is actually some + // text there. + notifyStoreListeners(StoreEvent.NOTICE, s); + } + + private String traceUser(String user) { + return debugusername ? user : ""; + } + + private String tracePassword(String password) { + return debugpassword ? password : + (password == null ? "" : ""); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IdleManager.java b/app/src/main/java/com/sun/mail/imap/IdleManager.java new file mode 100644 index 0000000000..781f58d5cd --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IdleManager.java @@ -0,0 +1,491 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.Socket; +import java.nio.*; +import java.nio.channels.*; +import java.util.*; +import java.util.logging.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; + +import javax.mail.*; + +import com.sun.mail.imap.protocol.IMAPProtocol; +import com.sun.mail.util.MailLogger; + +/** + * IdleManager uses the optional IMAP IDLE command + * (RFC 2177) + * to watch multiple folders for new messages. + * IdleManager uses an Executor to execute tasks in separate threads. + * An Executor is typically provided by an ExecutorService. + * For example, for a Java SE application: + *

+ *	ExecutorService es = Executors.newCachedThreadPool();
+ *	final IdleManager idleManager = new IdleManager(session, es);
+ * 
+ * For a Java EE 7 application: + *
+ *	{@literal @}Resource
+ *	ManagedExecutorService es;
+ *	final IdleManager idleManager = new IdleManager(session, es);
+ * 
+ * To watch for new messages in a folder, open the folder, register a listener, + * and ask the IdleManager to watch the folder: + *
+ *	Folder folder = store.getFolder("INBOX");
+ *	folder.open(Folder.READ_WRITE);
+ *	folder.addMessageCountListener(new MessageCountAdapter() {
+ *	    public void messagesAdded(MessageCountEvent ev) {
+ *		Folder folder = (Folder)ev.getSource();
+ *		Message[] msgs = ev.getMessages();
+ *		System.out.println("Folder: " + folder +
+ *		    " got " + msgs.length + " new messages");
+ *		try {
+ *		    // process new messages
+ *		    idleManager.watch(folder); // keep watching for new messages
+ *		} catch (MessagingException mex) {
+ *		    // handle exception related to the Folder
+ *		}
+ *	    }
+ *	});
+ *	idleManager.watch(folder);
+ * 
+ * This delivers the events for each folder in a separate thread, NOT + * using the Executor. To deliver all events in a single thread + * using the Executor, set the following properties for the Session + * (once), and then add listeners and watch the folder as above. + *
+ *	// the following should be done once...
+ *	Properties props = session.getProperties();
+ *	props.put("mail.event.scope", "session"); // or "application"
+ *	props.put("mail.event.executor", es);
+ * 
+ * Note that, after processing new messages in your listener, or doing any + * other operations on the folder in any other thread, you need to tell + * the IdleManager to watch for more new messages. Unless, of course, you + * close the folder. + *

+ * The IdleManager is created with a Session, which it uses only to control + * debug output. A single IdleManager instance can watch multiple Folders + * from multiple Stores and multiple Sessions. + *

+ * Due to limitations in the Java SE nio support, a + * {@link java.nio.channels.SocketChannel SocketChannel} must be used instead + * of a {@link java.net.Socket Socket} to connect to the server. However, + * SocketChannels don't support all the features of Sockets, such as connecting + * through a SOCKS proxy server. SocketChannels also don't support + * simultaneous read and write, which means that the + * {@link com.sun.mail.imap.IMAPFolder#idle idle} method can't be used if + * SocketChannels are being used; use this IdleManager instead. + * To enable support for SocketChannels instead of Sockets, set the + * mail.imap.usesocketchannels property in the Session used to + * access the IMAP Folder. (Or mail.imaps.usesocketchannels if + * you're using the "imaps" protocol.) This will effect all connections in + * that Session, but you can create another Session without this property set + * if you need to use the features that are incompatible with SocketChannels. + *

+ * NOTE: The IdleManager, and all APIs and properties related to it, should + * be considered EXPERIMENTAL. They may be changed in the + * future in ways that are incompatible with applications using the + * current APIs. + * + * @since JavaMail 1.5.2 + */ +public class IdleManager { + private Executor es; + private Selector selector; + private MailLogger logger; + private volatile boolean die = false; + private volatile boolean running; + private Queue toWatch = new ConcurrentLinkedQueue<>(); + private Queue toAbort = new ConcurrentLinkedQueue<>(); + + /** + * Create an IdleManager. The Session is used only to configure + * debugging output. The Executor is used to create the + * "select" thread. + * + * @param session the Session containing configuration information + * @param es the Executor used to create threads + * @exception IOException for Selector failures + */ + public IdleManager(Session session, Executor es) throws IOException { + this.es = es; + logger = new MailLogger(this.getClass(), "DEBUG IMAP", + session.getDebug(), session.getDebugOut()); + selector = Selector.open(); + es.execute(new Runnable() { + @Override + public void run() { + logger.fine("IdleManager select starting"); + try { + running = true; + select(); + } finally { + running = false; + logger.fine("IdleManager select terminating"); + } + } + }); + } + + /** + * Is the IdleManager currently running? The IdleManager starts + * running when the Executor schedules its task. The IdleManager + * stops running after its task detects the stop request from the + * {@link #stop stop} method, or if it terminates abnormally due + * to an unexpected error. + * + * @return true if the IdleMaanger is running + * @since JavaMail 1.5.5 + */ + public boolean isRunning() { + return running; + } + + /** + * Watch the Folder for new messages and other events using the IMAP IDLE + * command. + * + * @param folder the folder to watch + * @exception MessagingException for errors related to the folder + */ + public void watch(Folder folder) + throws MessagingException { + if (die) // XXX - should be IllegalStateException? + throw new MessagingException("IdleManager is not running"); + if (!(folder instanceof IMAPFolder)) + throw new MessagingException("Can only watch IMAP folders"); + IMAPFolder ifolder = (IMAPFolder)folder; + SocketChannel sc = ifolder.getChannel(); + if (sc == null) { + if (folder.isOpen()) + throw new MessagingException( + "Folder is not using SocketChannels"); + else + throw new MessagingException("Folder is not open"); + } + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, "IdleManager watching {0}", + folderName(ifolder)); + // keep trying to start the IDLE command until we're successful. + // may block if we're in the middle of aborting an IDLE command. + int tries = 0; + while (!ifolder.startIdle(this)) { + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager.watch startIdle failed for {0}", + folderName(ifolder)); + tries++; + } + if (logger.isLoggable(Level.FINEST)) { + if (tries > 0) + logger.log(Level.FINEST, + "IdleManager.watch startIdle succeeded for {0}" + + " after " + tries + " tries", + folderName(ifolder)); + else + logger.log(Level.FINEST, + "IdleManager.watch startIdle succeeded for {0}", + folderName(ifolder)); + } + synchronized (this) { + toWatch.add(ifolder); + selector.wakeup(); + } + } + + /** + * Request that the specified folder abort an IDLE command. + * We can't do the abort directly because the DONE message needs + * to be sent through the (potentially) SSL socket, which means + * we need to be in blocking I/O mode. We can only switch to + * blocking I/O mode when not selecting, so wake up the selector, + * which will process this request when it wakes up. + */ + void requestAbort(IMAPFolder folder) { + toAbort.add(folder); + selector.wakeup(); + } + + /** + * Run the {@link java.nio.channels.Selector#select select} loop + * to poll each watched folder for events sent from the server. + */ + private void select() { + die = false; + try { + while (!die) { + watchAll(); + logger.finest("IdleManager waiting..."); + int ns = selector.select(); + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager selected {0} channels", ns); + if (die || Thread.currentThread().isInterrupted()) + break; + + /* + * Process any selected folders. We cancel the + * selection key for any selected folder, so if we + * need to continue watching that folder it's added + * to the toWatch list again. We can't actually + * register that folder again until the previous + * selection key is cancelled, so we call selectNow() + * just for the side effect of cancelling the selection + * keys. But if selectNow() selects something, we + * process it before adding folders from the toWatch + * queue. And so on until there is nothing to do, at + * which point it's safe to register folders from the + * toWatch queue. This should be "fair" since each + * selection key is used only once before being added + * to the toWatch list. + */ + do { + processKeys(); + } while (selector.selectNow() > 0 || !toAbort.isEmpty()); + } + } catch (InterruptedIOException ex) { + logger.log(Level.FINEST, "IdleManager interrupted", ex); + } catch (IOException ex) { + logger.log(Level.FINEST, "IdleManager got I/O exception", ex); + } catch (Exception ex) { + logger.log(Level.FINEST, "IdleManager got exception", ex); + } finally { + die = true; // prevent new watches in case of exception + logger.finest("IdleManager unwatchAll"); + try { + unwatchAll(); + selector.close(); + } catch (IOException ex2) { + // nothing to do... + logger.log(Level.FINEST, "IdleManager unwatch exception", ex2); + } + logger.fine("IdleManager exiting"); + } + } + + /** + * Register all of the folders in the queue with the selector, + * switching them to nonblocking I/O mode first. + */ + private void watchAll() { + /* + * Pull each of the folders from the toWatch queue + * and register it. + */ + IMAPFolder folder; + while ((folder = toWatch.poll()) != null) { + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager adding {0} to selector", folderName(folder)); + try { + SocketChannel sc = folder.getChannel(); + if (sc == null) + continue; + // has to be non-blocking to select + sc.configureBlocking(false); + sc.register(selector, SelectionKey.OP_READ, folder); + } catch (IOException ex) { + // oh well, nothing to do + logger.log(Level.FINEST, + "IdleManager can't register folder", ex); + } catch (CancelledKeyException ex) { + // this should never happen + logger.log(Level.FINEST, + "IdleManager can't register folder", ex); + } + } + } + + /** + * Process the selected keys. + */ + private void processKeys() throws IOException { + IMAPFolder folder; + + /* + * First, process any channels with data to read. + */ + Set selectedKeys = selector.selectedKeys(); + /* + * XXX - this is simpler, but it can fail with + * ConcurrentModificationException + * + for (SelectionKey sk : selectedKeys) { + selectedKeys.remove(sk); // only process each key once + ... + } + */ + Iterator it = selectedKeys.iterator(); + while (it.hasNext()) { + SelectionKey sk = it.next(); + it.remove(); // only process each key once + // have to cancel so we can switch back to blocking I/O mode + sk.cancel(); + folder = (IMAPFolder)sk.attachment(); + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager selected folder: {0}", folderName(folder)); + SelectableChannel sc = sk.channel(); + // switch back to blocking to allow normal I/O + sc.configureBlocking(true); + try { + if (folder.handleIdle(false)) { + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager continue watching folder {0}", + folderName(folder)); + // more to do with this folder, select on it again + toWatch.add(folder); + } else { + // done watching this folder, + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager done watching folder {0}", + folderName(folder)); + } + } catch (MessagingException ex) { + // something went wrong, stop watching this folder + logger.log(Level.FINEST, + "IdleManager got exception for folder: " + + folderName(folder), ex); + } + } + + /* + * Now, process any folders that we need to abort. + */ + while ((folder = toAbort.poll()) != null) { + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager aborting IDLE for folder: {0}", + folderName(folder)); + SocketChannel sc = folder.getChannel(); + if (sc == null) + continue; + SelectionKey sk = sc.keyFor(selector); + // have to cancel so we can switch back to blocking I/O mode + if (sk != null) + sk.cancel(); + // switch back to blocking to allow normal I/O + sc.configureBlocking(true); + + // if there's a read timeout, have to do the abort in a new thread + Socket sock = sc.socket(); + if (sock != null && sock.getSoTimeout() > 0) { + logger.finest("IdleManager requesting DONE with timeout"); + toWatch.remove(folder); + final IMAPFolder folder0 = folder; + es.execute(new Runnable() { + @Override + public void run() { + // send the DONE and wait for the response + folder0.idleAbortWait(); + } + }); + } else { + folder.idleAbort(); // send the DONE message + // watch for OK response to DONE + // XXX - what if we also added it above? should be a nop + toWatch.add(folder); + } + } + } + + /** + * Stop watching all folders. Cancel any selection keys and, + * most importantly, switch the channel back to blocking mode. + * If there's any folders waiting to be watched, need to abort + * them too. + */ + private void unwatchAll() { + IMAPFolder folder; + Set keys = selector.keys(); + for (SelectionKey sk : keys) { + // have to cancel so we can switch back to blocking I/O mode + sk.cancel(); + folder = (IMAPFolder)sk.attachment(); + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager no longer watching folder: {0}", + folderName(folder)); + SelectableChannel sc = sk.channel(); + // switch back to blocking to allow normal I/O + try { + sc.configureBlocking(true); + folder.idleAbortWait(); // send the DONE message and wait + } catch (IOException ex) { + // ignore it, channel might be closed + logger.log(Level.FINEST, + "IdleManager exception while aborting idle for folder: " + + folderName(folder), ex); + } + } + + /* + * Finally, process any folders waiting to be watched. + */ + while ((folder = toWatch.poll()) != null) { + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager aborting IDLE for unwatched folder: {0}", + folderName(folder)); + SocketChannel sc = folder.getChannel(); + if (sc == null) + continue; + try { + // channel should still be in blocking mode, but make sure + sc.configureBlocking(true); + folder.idleAbortWait(); // send the DONE message and wait + } catch (IOException ex) { + // ignore it, channel might be closed + logger.log(Level.FINEST, + "IdleManager exception while aborting idle for folder: " + + folderName(folder), ex); + } + } + } + + /** + * Stop the IdleManager. The IdleManager can not be restarted. + */ + public synchronized void stop() { + die = true; + logger.fine("IdleManager stopping"); + selector.wakeup(); + } + + /** + * Return the fully qualified name of the folder, for use in log messages. + * Essentially just the getURLName method, but ignoring the + * MessagingException that can never happen. + */ + private static String folderName(Folder folder) { + try { + return folder.getURLName().toString(); + } catch (MessagingException mex) { + // can't happen + return folder.getStore().toString() + "/" + folder.toString(); + } + } +} diff --git a/app/src/main/java/com/sun/mail/imap/MessageCache.java b/app/src/main/java/com/sun/mail/imap/MessageCache.java new file mode 100644 index 0000000000..86c7899aa1 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/MessageCache.java @@ -0,0 +1,443 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.PrintStream; +import java.util.*; +import java.util.logging.Level; + +import javax.mail.*; +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; + +/** + * A cache of IMAPMessage objects along with the + * mapping from message number to IMAP sequence number. + * + * All operations on this object are protected by the messageCacheLock + * in IMAPFolder. + */ +public class MessageCache { + /* + * The array of IMAPMessage objects. Elements of the array might + * be null if no one has asked for the message. The array expands + * as needed and might be larger than the number of messages in the + * folder. The "size" field indicates the number of entries that + * are valid. + */ + private IMAPMessage[] messages; + + /* + * A parallel array of sequence numbers for each message. If the + * array pointer is null, the sequence number of a message is just + * its message number. This is the common case, until a message is + * expunged. + */ + private int[] seqnums; + + /* + * The amount of the messages (and seqnum) array that is valid. + * Might be less than the actual size of the array. + */ + private int size; + + /** + * The folder these messages belong to. + */ + private IMAPFolder folder; + + // debugging logger + private MailLogger logger; + + /** + * Grow the array by at least this much, to avoid constantly + * reallocating the array. + */ + private static final int SLOP = 64; + + /** + * Construct a new message cache of the indicated size. + */ + MessageCache(IMAPFolder folder, IMAPStore store, int size) { + this.folder = folder; + logger = folder.logger.getSubLogger("messagecache", "DEBUG IMAP MC", + store.getMessageCacheDebug()); + if (logger.isLoggable(Level.CONFIG)) + logger.config("create cache of size " + size); + ensureCapacity(size, 1); + } + + /** + * Constructor for debugging and testing. + */ + MessageCache(int size, boolean debug) { + this.folder = null; + logger = new MailLogger( + this.getClass(), "messagecache", + "DEBUG IMAP MC", debug, System.out); + if (logger.isLoggable(Level.CONFIG)) + logger.config("create DEBUG cache of size " + size); + ensureCapacity(size, 1); + } + + /** + * Size of cache. + * + * @return the size of the cache + */ + public int size() { + return size; + } + + /** + * Get the message object for the indicated message number. + * If the message object hasn't been created, create it. + * + * @param msgnum the message number + * @return the message + */ + public IMAPMessage getMessage(int msgnum) { + // check range + if (msgnum < 1 || msgnum > size) + throw new ArrayIndexOutOfBoundsException( + "message number (" + msgnum + ") out of bounds (" + size + ")"); + IMAPMessage msg = messages[msgnum-1]; + if (msg == null) { + if (logger.isLoggable(Level.FINE)) + logger.fine("create message number " + msgnum); + msg = folder.newIMAPMessage(msgnum); + messages[msgnum-1] = msg; + // mark message expunged if no seqnum + if (seqnumOf(msgnum) <= 0) { + logger.fine("it's expunged!"); + msg.setExpunged(true); + } + } + return msg; + } + + /** + * Get the message object for the indicated sequence number. + * If the message object hasn't been created, create it. + * Return null if there's no message with that sequence number. + * + * @param seqnum the sequence number of the message + * @return the message + */ + public IMAPMessage getMessageBySeqnum(int seqnum) { + int msgnum = msgnumOf(seqnum); + if (msgnum < 0) { // XXX - < 1 ? + if (logger.isLoggable(Level.FINE)) + logger.fine("no message seqnum " + seqnum); + return null; + } else + return getMessage(msgnum); + } + + /** + * Expunge the message with the given sequence number. + * + * @param seqnum the sequence number of the message to expunge + */ + public void expungeMessage(int seqnum) { + int msgnum = msgnumOf(seqnum); + if (msgnum < 0) { + if (logger.isLoggable(Level.FINE)) + logger.fine("expunge no seqnum " + seqnum); + return; // XXX - should never happen + } + IMAPMessage msg = messages[msgnum-1]; + if (msg != null) { + if (logger.isLoggable(Level.FINE)) + logger.fine("expunge existing " + msgnum); + msg.setExpunged(true); + } + if (seqnums == null) { // time to fill it in + logger.fine("create seqnums array"); + seqnums = new int[messages.length]; + for (int i = 1; i < msgnum; i++) + seqnums[i-1] = i; + seqnums[msgnum - 1] = 0; + for (int i = msgnum + 1; i <= seqnums.length; i++) + seqnums[i-1] = i - 1; + } else { + seqnums[msgnum - 1] = 0; + for (int i = msgnum + 1; i <= seqnums.length; i++) { + assert seqnums[i-1] != 1; + if (seqnums[i-1] > 0) + seqnums[i-1]--; + } + } + } + + /** + * Remove all the expunged messages from the array, + * returning a list of removed message objects. + * + * @return the removed messages + */ + public IMAPMessage[] removeExpungedMessages() { + logger.fine("remove expunged messages"); + // list of expunged messages + List mlist = new ArrayList<>(); + + /* + * Walk through the array compressing it by copying + * higher numbered messages further down in the array, + * effectively removing expunged messages from the array. + * oldnum is the index we use to walk through the array. + * newnum is the index where we copy the next valid message. + * oldnum == newnum until we encounter an expunged message. + */ + int oldnum = 1; + int newnum = 1; + while (oldnum <= size) { + // is message expunged? + if (seqnumOf(oldnum) <= 0) { + IMAPMessage m = getMessage(oldnum); + mlist.add(m); + } else { + // keep this message + if (newnum != oldnum) { + // move message down in the array (compact array) + messages[newnum-1] = messages[oldnum-1]; + if (messages[newnum-1] != null) + messages[newnum-1].setMessageNumber(newnum); + } + newnum++; + } + oldnum++; + } + seqnums = null; + shrink(newnum, oldnum); + + IMAPMessage[] rmsgs = new IMAPMessage[mlist.size()]; + if (logger.isLoggable(Level.FINE)) + logger.fine("return " + rmsgs.length); + mlist.toArray(rmsgs); + return rmsgs; + } + + /** + * Remove expunged messages in msgs from the array, + * returning a list of removed message objects. + * All messages in msgs must be IMAPMessage objects + * from this folder. + * + * @param msgs the messages + * @return the removed messages + */ + public IMAPMessage[] removeExpungedMessages(Message[] msgs) { + logger.fine("remove expunged messages"); + // list of expunged messages + List mlist = new ArrayList<>(); + + /* + * Copy the message numbers of the expunged messages into + * a separate array and sort the array to make it easier to + * process later. + */ + int[] mnum = new int[msgs.length]; + for (int i = 0; i < msgs.length; i++) + mnum[i] = msgs[i].getMessageNumber(); + Arrays.sort(mnum); + + /* + * Walk through the array compressing it by copying + * higher numbered messages further down in the array, + * effectively removing expunged messages from the array. + * oldnum is the index we use to walk through the array. + * newnum is the index where we copy the next valid message. + * oldnum == newnum until we encounter an expunged message. + * + * Even though we know the message number of the first possibly + * expunged message, we still start scanning at message number 1 + * so that we can check whether there's any message whose + * sequence number is different than its message number. If there + * is, we can't throw away the seqnums array when we're done. + */ + int oldnum = 1; + int newnum = 1; + int mnumi = 0; // index into mnum + boolean keepSeqnums = false; + while (oldnum <= size) { + /* + * Are there still expunged messsages in msgs to consider, + * and is the message we're considering the next one in the + * list, and is it expunged? + */ + if (mnumi < mnum.length && + oldnum == mnum[mnumi] && + seqnumOf(oldnum) <= 0) { + IMAPMessage m = getMessage(oldnum); + mlist.add(m); + /* + * Just in case there are duplicate entries in the msgs array, + * we keep advancing mnumi past any duplicates, but of course + * stop when we get to the end of the array. + */ + while (mnumi < mnum.length && mnum[mnumi] <= oldnum) + mnumi++; // consider next message in array + } else { + // keep this message + if (newnum != oldnum) { + // move message down in the array (compact array) + messages[newnum-1] = messages[oldnum-1]; + if (messages[newnum-1] != null) + messages[newnum-1].setMessageNumber(newnum); + if (seqnums != null) + seqnums[newnum-1] = seqnums[oldnum-1]; + } + if (seqnums != null && seqnums[newnum-1] != newnum) + keepSeqnums = true; + newnum++; + } + oldnum++; + } + + if (!keepSeqnums) + seqnums = null; + shrink(newnum, oldnum); + + IMAPMessage[] rmsgs = new IMAPMessage[mlist.size()]; + if (logger.isLoggable(Level.FINE)) + logger.fine("return " + rmsgs.length); + mlist.toArray(rmsgs); + return rmsgs; + } + + /** + * Shrink the messages and seqnums arrays. newend is one past last + * valid element. oldend is one past the previous last valid element. + */ + private void shrink(int newend, int oldend) { + size = newend - 1; + if (logger.isLoggable(Level.FINE)) + logger.fine("size now " + size); + if (size == 0) { // no messages left + messages = null; + seqnums = null; + } else if (size > SLOP && size < messages.length / 2) { + // if array shrinks by too much, reallocate it + logger.fine("reallocate array"); + IMAPMessage[] newm = new IMAPMessage[size + SLOP]; + System.arraycopy(messages, 0, newm, 0, size); + messages = newm; + if (seqnums != null) { + int[] news = new int[size + SLOP]; + System.arraycopy(seqnums, 0, news, 0, size); + seqnums = news; + } + } else { + if (logger.isLoggable(Level.FINE)) + logger.fine("clean " + newend + " to " + oldend); + // clear out unused entries in array + for (int msgnum = newend; msgnum < oldend; msgnum++) { + messages[msgnum-1] = null; + if (seqnums != null) + seqnums[msgnum-1] = 0; + } + } + } + + /** + * Add count messages to the cache. + * newSeqNum is the sequence number of the first message added. + * + * @param count the number of messges + * @param newSeqNum sequence number of first message + */ + public void addMessages(int count, int newSeqNum) { + if (logger.isLoggable(Level.FINE)) + logger.fine("add " + count + " messages"); + // don't have to do anything other than making sure there's space + ensureCapacity(size + count, newSeqNum); + } + + /* + * Make sure the arrays are at least big enough to hold + * "newsize" messages. + */ + private void ensureCapacity(int newsize, int newSeqNum) { + if (messages == null) + messages = new IMAPMessage[newsize + SLOP]; + else if (messages.length < newsize) { + if (logger.isLoggable(Level.FINE)) + logger.fine("expand capacity to " + newsize); + IMAPMessage[] newm = new IMAPMessage[newsize + SLOP]; + System.arraycopy(messages, 0, newm, 0, messages.length); + messages = newm; + if (seqnums != null) { + int[] news = new int[newsize + SLOP]; + System.arraycopy(seqnums, 0, news, 0, seqnums.length); + for (int i = size; i < news.length; i++) + news[i] = newSeqNum++; + seqnums = news; + if (logger.isLoggable(Level.FINE)) + logger.fine("message " + newsize + + " has sequence number " + seqnums[newsize-1]); + } + } else if (newsize < size) { // shrinking? + // this should never happen + if (logger.isLoggable(Level.FINE)) + logger.fine("shrink capacity to " + newsize); + for (int msgnum = newsize + 1; msgnum <= size; msgnum++) { + messages[msgnum-1] = null; + if (seqnums != null) + seqnums[msgnum-1] = -1; + } + } + size = newsize; + } + + /** + * Return the sequence number for the given message number. + * + * @param msgnum the message number + * @return the sequence number + */ + public int seqnumOf(int msgnum) { + if (seqnums == null) + return msgnum; + else { + if (logger.isLoggable(Level.FINE)) + logger.fine("msgnum " + msgnum + " is seqnum " + + seqnums[msgnum-1]); + return seqnums[msgnum-1]; + } + } + + /** + * Return the message number for the given sequence number. + */ + private int msgnumOf(int seqnum) { + if (seqnums == null) + return seqnum; + if (seqnum < 1) { // should never happen + if (logger.isLoggable(Level.FINE)) + logger.fine("bad seqnum " + seqnum); + return -1; + } + for (int msgnum = seqnum; msgnum <= size; msgnum++) { + if (seqnums[msgnum-1] == seqnum) + return msgnum; + if (seqnums[msgnum-1] > seqnum) + break; // message doesn't exist + } + return -1; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/MessageVanishedEvent.java b/app/src/main/java/com/sun/mail/imap/MessageVanishedEvent.java new file mode 100644 index 0000000000..056cabc68a --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/MessageVanishedEvent.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.event.MessageCountEvent; + +/** + * This class provides notification of messages that have been removed + * since the folder was last synchronized. + * + * @since JavaMail 1.5.1 + * @author Bill Shannon + */ + +public class MessageVanishedEvent extends MessageCountEvent { + + /** + * The message UIDs. + */ + private long[] uids; + + // a reusable empty array + private static final Message[] noMessages = { }; + + private static final long serialVersionUID = 2142028010250024922L; + + /** + * Constructor. + * + * @param folder the containing folder + * @param uids the UIDs for the vanished messages + */ + public MessageVanishedEvent(Folder folder, long[] uids) { + super(folder, REMOVED, true, noMessages); + this.uids = uids; + } + + /** + * Return the UIDs for this event. + * + * @return the UIDs + */ + public long[] getUIDs() { + return uids; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/ModifiedSinceTerm.java b/app/src/main/java/com/sun/mail/imap/ModifiedSinceTerm.java new file mode 100644 index 0000000000..7332266f25 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/ModifiedSinceTerm.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import javax.mail.Message; +import javax.mail.search.SearchTerm; + +/** + * Find messages that have been modified since a given MODSEQ value. + * Relies on the server implementing the CONDSTORE extension + * (RFC 4551). + * + * @since JavaMail 1.5.1 + * @author Bill Shannon + */ +public final class ModifiedSinceTerm extends SearchTerm { + + private long modseq; + + private static final long serialVersionUID = 5151457469634727992L; + + /** + * Constructor. + * + * @param modseq modification sequence number + */ + public ModifiedSinceTerm(long modseq) { + this.modseq = modseq; + } + + /** + * Return the modseq. + * + * @return the modseq + */ + public long getModSeq() { + return modseq; + } + + /** + * The match method. + * + * @param msg the date comparator is applied to this Message's + * MODSEQ + * @return true if the comparison succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + long m; + + try { + if (msg instanceof IMAPMessage) + m = ((IMAPMessage)msg).getModSeq(); + else + return false; + } catch (Exception e) { + return false; + } + + return m >= modseq; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ModifiedSinceTerm)) + return false; + return modseq == ((ModifiedSinceTerm)obj).modseq; + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return (int)modseq; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/OlderTerm.java b/app/src/main/java/com/sun/mail/imap/OlderTerm.java new file mode 100644 index 0000000000..19857c4dfd --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/OlderTerm.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.util.Date; +import javax.mail.Message; +import javax.mail.search.SearchTerm; + +/** + * Find messages that are older than a given interval (in seconds). + * Relies on the server implementing the WITHIN search extension + * (RFC 5032). + * + * @since JavaMail 1.5.1 + * @author Bill Shannon + */ +public final class OlderTerm extends SearchTerm { + + private int interval; + + private static final long serialVersionUID = 3951078948727995682L; + + /** + * Constructor. + * + * @param interval number of seconds older + */ + public OlderTerm(int interval) { + this.interval = interval; + } + + /** + * Return the interval. + * + * @return the interval + */ + public int getInterval() { + return interval; + } + + /** + * The match method. + * + * @param msg the date comparator is applied to this Message's + * received date + * @return true if the comparison succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + Date d; + + try { + d = msg.getReceivedDate(); + } catch (Exception e) { + return false; + } + + if (d == null) + return false; + + return d.getTime() <= + System.currentTimeMillis() - ((long)interval * 1000); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof OlderTerm)) + return false; + return interval == ((OlderTerm)obj).interval; + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return interval; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/ReferralException.java b/app/src/main/java/com/sun/mail/imap/ReferralException.java new file mode 100644 index 0000000000..f4a335c163 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/ReferralException.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import javax.mail.AuthenticationFailedException; + +/** + * A special kind of AuthenticationFailedException that indicates that + * the reason for the failure was an IMAP REFERRAL in the response code. + * See RFC 2221 for details. + * + * @since JavaMail 1.5.5 + */ + +public class ReferralException extends AuthenticationFailedException { + + private String url; + private String text; + + private static final long serialVersionUID = -3414063558596287683L; + + /** + * Constructs an ReferralException with the specified URL and text. + * + * @param text the detail message + * @param url the URL + */ + public ReferralException(String url, String text) { + super("[REFERRAL " + url + "] " + text); + this.url = url; + this.text = text; + } + + /** + * Return the IMAP URL in the referral. + * + * @return the IMAP URL + */ + public String getUrl() { + return url; + } + + /** + * Return the text sent by the server along with the referral. + * + * @return the text + */ + public String getText() { + return text; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/ResyncData.java b/app/src/main/java/com/sun/mail/imap/ResyncData.java new file mode 100644 index 0000000000..f5c295e426 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/ResyncData.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import com.sun.mail.imap.protocol.UIDSet; + +/** + * Resynchronization data as defined by the QRESYNC extension + * (RFC 5162). + * An instance of ResyncData is supplied to the + * {@link com.sun.mail.imap.IMAPFolder#open(int,com.sun.mail.imap.ResyncData) + * IMAPFolder open} method. + * The CONDSTORE ResyncData instance is used to enable the + * CONDSTORE extension + * (RFC 4551). + * A ResyncData instance with uidvalidity and modseq values + * is used to enable the QRESYNC extension. + * + * @since JavaMail 1.5.1 + * @author Bill Shannon + */ + +public class ResyncData { + private long uidvalidity = -1; + private long modseq = -1; + private UIDSet[] uids = null; + + /** + * Used to enable only the CONDSTORE extension. + */ + public static final ResyncData CONDSTORE = new ResyncData(-1, -1); + + /** + * Used to report on changes since the specified modseq. + * If the UIDVALIDITY of the folder has changed, no message + * changes will be reported. The application must check the + * UIDVALIDITY of the folder after open to make sure it's + * the expected folder. + * + * @param uidvalidity the UIDVALIDITY + * @param modseq the MODSEQ + */ + public ResyncData(long uidvalidity, long modseq) { + this.uidvalidity = uidvalidity; + this.modseq = modseq; + this.uids = null; + } + + /** + * Used to limit the reported message changes to those with UIDs + * in the specified range. + * + * @param uidvalidity the UIDVALIDITY + * @param modseq the MODSEQ + * @param uidFirst the first UID + * @param uidLast the last UID + */ + public ResyncData(long uidvalidity, long modseq, + long uidFirst, long uidLast) { + this.uidvalidity = uidvalidity; + this.modseq = modseq; + this.uids = new UIDSet[] { new UIDSet(uidFirst, uidLast) }; + } + + /** + * Used to limit the reported message changes to those with the + * specified UIDs. + * + * @param uidvalidity the UIDVALIDITY + * @param modseq the MODSEQ + * @param uids the UID values + */ + public ResyncData(long uidvalidity, long modseq, long[] uids) { + this.uidvalidity = uidvalidity; + this.modseq = modseq; + this.uids = UIDSet.createUIDSets(uids); + } + + /** + * Get the UIDVALIDITY value specified when this instance was created. + * + * @return the UIDVALIDITY value + */ + public long getUIDValidity() { + return uidvalidity; + } + + /** + * Get the MODSEQ value specified when this instance was created. + * + * @return the MODSEQ value + */ + public long getModSeq() { + return modseq; + } + + /* + * Package private. IMAPProtocol gets this data indirectly + * using Utility.getResyncUIDSet(). + */ + UIDSet[] getUIDSet() { + return uids; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/Rights.java b/app/src/main/java/com/sun/mail/imap/Rights.java new file mode 100644 index 0000000000..d9c4ce0298 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/Rights.java @@ -0,0 +1,444 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.util.*; + +/** + * The Rights class represents the set of rights for an authentication + * identifier (for instance, a user or a group).

+ * + * A right is represented by the Rights.Right + * inner class.

+ * + * A set of standard rights are predefined (see RFC 2086). Most folder + * implementations are expected to support these rights. Some + * implementations may also support site-defined rights.

+ * + * The following code sample illustrates how to examine your + * rights for a folder. + *

+ *
+ * Rights rights = folder.myRights();
+ *
+ * // Check if I can write this folder
+ * if (rights.contains(Rights.Right.WRITE))
+ *	System.out.println("Can write folder");
+ *
+ * // Now give Joe all my rights, except the ability to write the folder
+ * rights.remove(Rights.Right.WRITE);
+ * ACL acl = new ACL("joe", rights);
+ * folder.setACL(acl);
+ * 
+ *

+ * + * @author Bill Shannon + */ + +public class Rights implements Cloneable { + + private boolean[] rights = new boolean[128]; // XXX + + /** + * This inner class represents an individual right. A set + * of standard rights objects are predefined here. + */ + public static final class Right { + private static Right[] cache = new Right[128]; + + // XXX - initialization order? + /** + * Lookup - mailbox is visible to LIST/LSUB commands. + */ + public static final Right LOOKUP = getInstance('l'); + + /** + * Read - SELECT the mailbox, perform CHECK, FETCH, PARTIAL, + * SEARCH, COPY from mailbox + */ + public static final Right READ = getInstance('r'); + + /** + * Keep seen/unseen information across sessions - STORE \SEEN flag. + */ + public static final Right KEEP_SEEN = getInstance('s'); + + /** + * Write - STORE flags other than \SEEN and \DELETED. + */ + public static final Right WRITE = getInstance('w'); + + /** + * Insert - perform APPEND, COPY into mailbox. + */ + public static final Right INSERT = getInstance('i'); + + /** + * Post - send mail to submission address for mailbox, + * not enforced by IMAP4 itself. + */ + public static final Right POST = getInstance('p'); + + /** + * Create - CREATE new sub-mailboxes in any implementation-defined + * hierarchy, RENAME or DELETE mailbox. + */ + public static final Right CREATE = getInstance('c'); + + /** + * Delete - STORE \DELETED flag, perform EXPUNGE. + */ + public static final Right DELETE = getInstance('d'); + + /** + * Administer - perform SETACL. + */ + public static final Right ADMINISTER = getInstance('a'); + + char right; // the right represented by this Right object + + /** + * Private constructor used only by getInstance. + */ + private Right(char right) { + if ((int)right >= 128) + throw new IllegalArgumentException("Right must be ASCII"); + this.right = right; + } + + /** + * Get a Right object representing the specified character. + * Characters are assigned per RFC 2086. + * + * @param right the character representing the right + * @return the Right object + */ + public static synchronized Right getInstance(char right) { + if ((int)right >= 128) + throw new IllegalArgumentException("Right must be ASCII"); + if (cache[(int)right] == null) + cache[(int)right] = new Right(right); + return cache[(int)right]; + } + + @Override + public String toString() { + return String.valueOf(right); + } + } + + + /** + * Construct an empty Rights object. + */ + public Rights() { } + + /** + * Construct a Rights object initialized with the given rights. + * + * @param rights the rights for initialization + */ + public Rights(Rights rights) { + System.arraycopy(rights.rights, 0, this.rights, 0, this.rights.length); + } + + /** + * Construct a Rights object initialized with the given rights. + * + * @param rights the rights for initialization + */ + public Rights(String rights) { + for (int i = 0; i < rights.length(); i++) + add(Right.getInstance(rights.charAt(i))); + } + + /** + * Construct a Rights object initialized with the given right. + * + * @param right the right for initialization + */ + public Rights(Right right) { + this.rights[(int)right.right] = true; + } + + /** + * Add the specified right to this Rights object. + * + * @param right the right to add + */ + public void add(Right right) { + this.rights[(int)right.right] = true; + } + + /** + * Add all the rights in the given Rights object to this + * Rights object. + * + * @param rights Rights object + */ + public void add(Rights rights) { + for (int i = 0; i < rights.rights.length; i++) + if (rights.rights[i]) + this.rights[i] = true; + } + + /** + * Remove the specified right from this Rights object. + * + * @param right the right to be removed + */ + public void remove(Right right) { + this.rights[(int)right.right] = false; + } + + /** + * Remove all rights in the given Rights object from this + * Rights object. + * + * @param rights the rights to be removed + */ + public void remove(Rights rights) { + for (int i = 0; i < rights.rights.length; i++) + if (rights.rights[i]) + this.rights[i] = false; + } + + /** + * Check whether the specified right is present in this Rights object. + * + * @param right the Right to check + * @return true of the given right is present, otherwise false. + */ + public boolean contains(Right right) { + return this.rights[(int)right.right]; + } + + /** + * Check whether all the rights in the specified Rights object are + * present in this Rights object. + * + * @param rights the Rights to check + * @return true if all rights in the given Rights object are present, + * otherwise false. + */ + public boolean contains(Rights rights) { + for (int i = 0; i < rights.rights.length; i++) + if (rights.rights[i] && !this.rights[i]) + return false; + + // If we've made it till here, return true + return true; + } + + /** + * Check whether the two Rights objects are equal. + * + * @return true if they're equal + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Rights)) + return false; + + Rights rights = (Rights)obj; + + for (int i = 0; i < rights.rights.length; i++) + if (rights.rights[i] != this.rights[i]) + return false; + + return true; + } + + /** + * Compute a hash code for this Rights object. + * + * @return the hash code + */ + @Override + public int hashCode() { + int hash = 0; + for (int i = 0; i < this.rights.length; i++) + if (this.rights[i]) + hash++; + return hash; + } + + /** + * Return all the rights in this Rights object. Returns + * an array of size zero if no rights are set. + * + * @return array of Rights.Right objects representing rights + */ + public Right[] getRights() { + List v = new ArrayList<>(); + for (int i = 0; i < this.rights.length; i++) + if (this.rights[i]) + v.add(Right.getInstance((char)i)); + return v.toArray(new Right[v.size()]); + } + + /** + * Returns a clone of this Rights object. + */ + @Override + public Object clone() { + Rights r = null; + try { + r = (Rights)super.clone(); + r.rights = new boolean[128]; + System.arraycopy(this.rights, 0, r.rights, 0, this.rights.length); + } catch (CloneNotSupportedException cex) { + // ignore, can't happen + } + return r; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < this.rights.length; i++) + if (this.rights[i]) + sb.append((char)i); + return sb.toString(); + } + + /***** + public static void main(String argv[]) throws Exception { + // a new rights object + Rights f1 = new Rights(); + f1.add(Rights.Right.READ); + f1.add(Rights.Right.WRITE); + f1.add(Rights.Right.CREATE); + f1.add(Rights.Right.DELETE); + + // check copy constructor + Rights fc = new Rights(f1); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // check clone + fc = (Rights)f1.clone(); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // add a right and make sure it still works right + f1.add(Rights.Right.ADMINISTER); + + // shouldn't be equal here + if (!f1.equals(fc) && !fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // check clone + fc = (Rights)f1.clone(); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + fc.add(Rights.Right.INSERT); + if (!f1.equals(fc) && !fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // check copy constructor + fc = new Rights(f1); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // another new rights object + Rights f2 = new Rights(Rights.Right.READ); + f2.add(Rights.Right.WRITE); + + if (f1.contains(Rights.Right.READ)) + System.out.println("success"); + else + System.out.println("fail"); + + if (f1.contains(Rights.Right.WRITE)) + System.out.println("success"); + else + System.out.println("fail"); + + if (f1.contains(Rights.Right.CREATE)) + System.out.println("success"); + else + System.out.println("fail"); + + if (f1.contains(Rights.Right.DELETE)) + System.out.println("success"); + else + System.out.println("fail"); + + if (f2.contains(Rights.Right.WRITE)) + System.out.println("success"); + else + System.out.println("fail"); + + + System.out.println("----------------"); + + Right[] r = f1.getRights(); + for (int i = 0; i < r.length; i++) + System.out.println(r[i]); + System.out.println("----------------"); + + if (f1.contains(f2)) // this should be true + System.out.println("success"); + else + System.out.println("fail"); + + if (!f2.contains(f1)) // this should be false + System.out.println("success"); + else + System.out.println("fail"); + + Rights f3 = new Rights(); + f3.add(Rights.Right.READ); + f3.add(Rights.Right.WRITE); + f3.add(Rights.Right.CREATE); + f3.add(Rights.Right.DELETE); + f3.add(Rights.Right.ADMINISTER); + f3.add(Rights.Right.LOOKUP); + + f1.add(Rights.Right.LOOKUP); + + if (f1.equals(f3)) + System.out.println("equals success"); + else + System.out.println("fail"); + if (f3.equals(f1)) + System.out.println("equals success"); + else + System.out.println("fail"); + System.out.println("f1 hash code " + f1.hashCode()); + System.out.println("f3 hash code " + f3.hashCode()); + if (f1.hashCode() == f3.hashCode()) + System.out.println("success"); + else + System.out.println("fail"); + } + ****/ +} diff --git a/app/src/main/java/com/sun/mail/imap/SortTerm.java b/app/src/main/java/com/sun/mail/imap/SortTerm.java new file mode 100644 index 0000000000..6f624e8434 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/SortTerm.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +/** + * A particular sort criteria, as defined by + * RFC 5256. + * Sort criteria are used with the + * {@link IMAPFolder#getSortedMessages getSortedMessages} method. + * Multiple sort criteria are specified in an array with the order in + * the array specifying the order in which the sort criteria are applied. + * + * @since JavaMail 1.4.4 + */ +public final class SortTerm { + /** + * Sort by message arrival date and time. + */ + public static final SortTerm ARRIVAL = new SortTerm("ARRIVAL"); + + /** + * Sort by email address of first Cc recipient. + */ + public static final SortTerm CC = new SortTerm("CC"); + + /** + * Sort by sent date and time. + */ + public static final SortTerm DATE = new SortTerm("DATE"); + + /** + * Sort by first From email address. + */ + public static final SortTerm FROM = new SortTerm("FROM"); + + /** + * Reverse the sort order of the following item. + */ + public static final SortTerm REVERSE = new SortTerm("REVERSE"); + + /** + * Sort by the message size. + */ + public static final SortTerm SIZE = new SortTerm("SIZE"); + + /** + * Sort by the base subject text. Note that the "base subject" + * is defined by RFC 5256 and doesn't include items such as "Re:" + * in the subject header. + */ + public static final SortTerm SUBJECT = new SortTerm("SUBJECT"); + + /** + * Sort by email address of first To recipient. + */ + public static final SortTerm TO = new SortTerm("TO"); + + private String term; + private SortTerm(String term) { + this.term = term; + } + + @Override + public String toString() { + return term; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/Utility.java b/app/src/main/java/com/sun/mail/imap/Utility.java new file mode 100644 index 0000000000..43f19983e3 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/Utility.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.util.Arrays; +import java.util.Comparator; + +import javax.mail.*; + +import com.sun.mail.imap.protocol.MessageSet; +import com.sun.mail.imap.protocol.UIDSet; +import java.util.ArrayList; +import java.util.List; + +/** + * Holder for some static utility methods. + * + * @author John Mani + * @author Bill Shannon + */ + +public final class Utility { + + // Cannot be initialized + private Utility() { } + + /** + * Run thru the given array of messages, apply the given + * Condition on each message and generate sets of contiguous + * sequence-numbers for the successful messages. If a message + * in the given array is found to be expunged, it is ignored. + * + * ASSERT: Since this method uses and returns message sequence + * numbers, you should use this method only when holding the + * messageCacheLock. + * + * @param msgs the messages + * @param cond the condition to check + * @return the MessageSet array + */ + public static MessageSet[] toMessageSet(Message[] msgs, Condition cond) { + List v = new ArrayList<>(1); + int current, next; + + IMAPMessage msg; + for (int i = 0; i < msgs.length; i++) { + msg = (IMAPMessage)msgs[i]; + if (msg.isExpunged()) // expunged message, skip it + continue; + + current = msg.getSequenceNumber(); + // Apply the condition. If it fails, skip it. + if ((cond != null) && !cond.test(msg)) + continue; + + MessageSet set = new MessageSet(); + set.start = current; + + // Look for contiguous sequence numbers + for (++i; i < msgs.length; i++) { + // get next message + msg = (IMAPMessage)msgs[i]; + + if (msg.isExpunged()) // expunged message, skip it + continue; + next = msg.getSequenceNumber(); + + // Does this message match our condition ? + if ((cond != null) && !cond.test(msg)) + continue; + + if (next == current+1) + current = next; + else { // break in sequence + // We need to reexamine this message at the top of + // the outer loop, so decrement 'i' to cancel the + // outer loop's autoincrement + i--; + break; + } + } + set.end = current; + v.add(set); + } + + if (v.isEmpty()) // No valid messages + return null; + else { + return v.toArray(new MessageSet[v.size()]); + } + } + /** + * Sort (a copy of) the given array of messages and then + * run thru the sorted array of messages, apply the given + * Condition on each message and generate sets of contiguous + * sequence-numbers for the successful messages. If a message + * in the given array is found to be expunged, it is ignored. + * + * ASSERT: Since this method uses and returns message sequence + * numbers, you should use this method only when holding the + * messageCacheLock. + * + * @param msgs the messages + * @param cond the condition to check + * @return the MessageSet array + * @since JavaMail 1.5.4 + */ + public static MessageSet[] toMessageSetSorted(Message[] msgs, + Condition cond) { + /* + * XXX - This is quick and dirty. A more efficient strategy would be + * to generate an array of message numbers by applying the condition + * (with zero indicating the message doesn't satisfy the condition), + * sort it, and then convert it to a MessageSet skipping all the zeroes. + */ + msgs = msgs.clone(); + Arrays.sort(msgs, + new Comparator() { + @Override + public int compare(Message msg1, Message msg2) { + return msg1.getMessageNumber() - msg2.getMessageNumber(); + } + }); + return toMessageSet(msgs, cond); + } + + /** + * Return UIDSets for the messages. Note that the UIDs + * must have already been fetched for the messages. + * + * @param msgs the messages + * @return the UIDSet array + */ + public static UIDSet[] toUIDSet(Message[] msgs) { + List v = new ArrayList<>(1); + long current, next; + + IMAPMessage msg; + for (int i = 0; i < msgs.length; i++) { + msg = (IMAPMessage)msgs[i]; + if (msg.isExpunged()) // expunged message, skip it + continue; + + current = msg.getUID(); + + UIDSet set = new UIDSet(); + set.start = current; + + // Look for contiguous UIDs + for (++i; i < msgs.length; i++) { + // get next message + msg = (IMAPMessage)msgs[i]; + + if (msg.isExpunged()) // expunged message, skip it + continue; + next = msg.getUID(); + + if (next == current+1) + current = next; + else { // break in sequence + // We need to reexamine this message at the top of + // the outer loop, so decrement 'i' to cancel the + // outer loop's autoincrement + i--; + break; + } + } + set.end = current; + v.add(set); + } + + if (v.isEmpty()) // No valid messages + return null; + else { + return v.toArray(new UIDSet[v.size()]); + } + } + + /** + * Make the ResyncData UIDSet available to IMAPProtocol, + * which is in a different package. Note that this class + * is not included in the public javadocs, thus "hiding" + * this method. + * + * @param rd the ResyncData + * @return the UIDSet array + * @since JavaMail 1.5.1 + */ + public static UIDSet[] getResyncUIDSet(ResyncData rd) { + return rd.getUIDSet(); + } + + /** + * This interface defines the test to be executed in + * toMessageSet(). + */ + public static interface Condition { + public boolean test(IMAPMessage message); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/YoungerTerm.java b/app/src/main/java/com/sun/mail/imap/YoungerTerm.java new file mode 100644 index 0000000000..e8264aed89 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/YoungerTerm.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.util.Date; +import javax.mail.Message; +import javax.mail.search.SearchTerm; + +/** + * Find messages that are younger than a given interval (in seconds). + * Relies on the server implementing the WITHIN search extension + * (RFC 5032). + * + * @since JavaMail 1.5.1 + * @author Bill Shannon + */ +public final class YoungerTerm extends SearchTerm { + + private int interval; + + private static final long serialVersionUID = 1592714210688163496L; + + /** + * Constructor. + * + * @param interval number of seconds younger + */ + public YoungerTerm(int interval) { + this.interval = interval; + } + + /** + * Return the interval. + * + * @return the interval + */ + public int getInterval() { + return interval; + } + + /** + * The match method. + * + * @param msg the date comparator is applied to this Message's + * received date + * @return true if the comparison succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + Date d; + + try { + d = msg.getReceivedDate(); + } catch (Exception e) { + return false; + } + + if (d == null) + return false; + + return d.getTime() >= + System.currentTimeMillis() - ((long)interval * 1000); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof YoungerTerm)) + return false; + return interval == ((YoungerTerm)obj).interval; + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return interval; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/package.html b/app/src/main/java/com/sun/mail/imap/package.html new file mode 100644 index 0000000000..1103d26e8d --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/package.html @@ -0,0 +1,1002 @@ + + + + + + +com.sun.mail.imap package + + + +

+An IMAP protocol provider for the Jakarta Mail API +that provides access to an IMAP message store. +Both the IMAP4 and IMAP4rev1 protocols are supported. +Refer to +RFC 3501 +for more information. +The IMAP protocol provider also supports many IMAP extensions (described below). +Note that the server needs to support these extensions (and not all servers do) +in order to use the support in the IMAP provider. +You can query the server for support of these extensions using the +{@link com.sun.mail.imap.IMAPStore#hasCapability IMAPStore hasCapability} +method using the capability name defined by the extension +(see the appropriate RFC) after connecting to the server. +

+UIDPLUS Support +

+The IMAP UIDPLUS extension +(RFC 4315) +is supported via the IMAPFolder methods +{@link com.sun.mail.imap.IMAPFolder#addMessages addMessages}, +{@link com.sun.mail.imap.IMAPFolder#appendUIDMessages appendUIDMessages}, and +{@link com.sun.mail.imap.IMAPFolder#copyUIDMessages copyUIDMessages}. +

+MOVE Support +

+The IMAP MOVE extension +(RFC 6851) +is supported via the IMAPFolder methods +{@link com.sun.mail.imap.IMAPFolder#moveMessages moveMessages} and +{@link com.sun.mail.imap.IMAPFolder#moveUIDMessages moveUIDMessages}. +

+SASL Support +

+The IMAP protocol provider can use SASL +(RFC 4422) +authentication mechanisms on systems that support the +javax.security.sasl APIs. +The SASL-IR +(RFC 4959) +capability is also supported. +In addition to the SASL mechanisms that are built into +the SASL implementation, users can also provide additional +SASL mechanisms of their own design to support custom authentication +schemes. See the + +Java SASL API Programming and Deployment Guide for details. +Note that the current implementation doesn't support SASL mechanisms +that provide their own integrity or confidentiality layer. +

+OAuth 2.0 Support +

+Support for OAuth 2.0 authentication via the + +XOAUTH2 authentication mechanism is provided either through the SASL +support described above or as a built-in authentication mechanism in the +IMAP provider. +The OAuth 2.0 Access Token should be passed as the password for this mechanism. +See +OAuth2 Support for details. +

+Connection Pool +

+A connected IMAPStore maintains a pool of IMAP protocol objects for +use in communicating with the IMAP server. The IMAPStore will create +the initial AUTHENTICATED connection and seed the pool with this +connection. As folders are opened and new IMAP protocol objects are +needed, the IMAPStore will provide them from the connection pool, +or create them if none are available. When a folder is closed, +its IMAP protocol object is returned to the connection pool if the +pool is not over capacity. +

+

+A mechanism is provided for timing out idle connection pool IMAP +protocol objects. Timed out connections are closed and removed (pruned) +from the connection pool. +

+

+The connected IMAPStore object may or may not maintain a separate IMAP +protocol object that provides the store a dedicated connection to the +IMAP server. This is provided mainly for compatibility with previous +implementations of the IMAP protocol provider. +

+QUOTA Support +

+The IMAP QUOTA extension +(RFC 2087) +is supported via the +{@link javax.mail.QuotaAwareStore QuotaAwareStore} interface implemented by +{@link com.sun.mail.imap.IMAPStore IMAPStore}, and the +{@link com.sun.mail.imap.IMAPFolder#getQuota IMAPFolder getQuota} and +{@link com.sun.mail.imap.IMAPFolder#setQuota IMAPFolder setQuota} methods. +ACL Support +

+The IMAP ACL extension +(RFC 2086) +is supported via the +{@link com.sun.mail.imap.Rights Rights} class and the IMAPFolder methods +{@link com.sun.mail.imap.IMAPFolder#getACL getACL}, +{@link com.sun.mail.imap.IMAPFolder#addACL addACL}, +{@link com.sun.mail.imap.IMAPFolder#removeACL removeACL}, +{@link com.sun.mail.imap.IMAPFolder#addRights addRights}, +{@link com.sun.mail.imap.IMAPFolder#removeRights removeRights}, +{@link com.sun.mail.imap.IMAPFolder#listRights listRights}, and +{@link com.sun.mail.imap.IMAPFolder#myRights myRights}. +

+SORT Support +

+The IMAP SORT extension +(RFC 5256) +is supported via the +{@link com.sun.mail.imap.SortTerm SortTerm} class and the IMAPFolder +{@link com.sun.mail.imap.IMAPFolder#getSortedMessages getSortedMessages} +methods. +

+CONDSTORE and QRESYNC Support +

+Basic support is provided for the IMAP CONDSTORE +(RFC 4551) +and QRESYNC +(RFC 5162) +extensions for the purpose of resynchronizing a folder after offline operation. +Of course, the server must support these extensions. +Use of these extensions is enabled by using the new +{@link com.sun.mail.imap.IMAPFolder#open(int,com.sun.mail.imap.ResyncData) +IMAPFolder open} method and supplying an appropriate +{@link com.sun.mail.imap.ResyncData ResyncData} instance. +Using +{@link com.sun.mail.imap.ResyncData#CONDSTORE ResyncData.CONDSTORE} +enables the CONDSTORE extension, which allows you to discover the +modification sequence number (modseq) of messages using the +{@link com.sun.mail.imap.IMAPMessage#getModSeq IMAPMessage getModSeq} +method and the +{@link com.sun.mail.imap.IMAPFolder#getHighestModSeq +IMAPFolder getHighestModSeq} method. +Using a +{@link com.sun.mail.imap.ResyncData ResyncData} instance with appropriate +values also allows the server to report any changes in messages since the last +resynchronization. +The changes are reported as a list of +{@link javax.mail.event.MailEvent MailEvent} instances. +The special +{@link com.sun.mail.imap.MessageVanishedEvent MessageVanishedEvent} reports on +UIDs of messages that have been removed since the last resynchronization. +A +{@link javax.mail.event.MessageChangedEvent MessageChangedEvent} reports on +changes to flags of messages. +For example: +

+
+	Folder folder = store.getFolder("whatever");
+	IMAPFolder ifolder = (IMAPFolder)folder;
+	List<MailEvent> events = ifolder.open(Folder.READ_WRITE,
+		    new ResyncData(prevUidValidity, prevModSeq));
+	for (MailEvent ev : events) {
+	    if (ev instanceOf MessageChangedEvent) {
+		// process flag changes
+	    } else if (ev instanceof MessageVanishedEvent) {
+		// process messages that were removed
+	    }
+	}
+
+

+See the referenced RFCs for more details on these IMAP extensions. +

+WITHIN Search Support +

+The IMAP WITHIN search extension +(RFC 5032) +is supported via the +{@link com.sun.mail.imap.YoungerTerm YoungerTerm} and +{@link com.sun.mail.imap.OlderTerm OlderTerm} +{@link javax.mail.search.SearchTerm SearchTerms}, which can be used as follows: +

+
+	// search for messages delivered in the last day
+	Message[] msgs = folder.search(new YoungerTerm(24 * 60 * 60));
+
+LOGIN-REFERRAL Support +

+The IMAP LOGIN-REFERRAL extension +(RFC 2221) +is supported. +If a login referral is received when connecting or when authentication fails, a +{@link com.sun.mail.imap.ReferralException ReferralException} is thrown. +A referral can also occur when login succeeds. By default, no exception is +thrown in this case. To force an exception to be thrown and the authentication +to fail, set the mail.imap.referralexception property to "true". +

+COMPRESS Support +

+The IMAP COMPRESS extension +(RFC 4978) +is supported. +If the server supports the extension and the +mail.imap.compress.enable property is set to "true", +compression will be enabled. +

+UTF-8 Support +

+The IMAP UTF8 extension +(RFC 6855) +is supported. +If the server supports the extension, the client will enable use of UTF-8, +allowing use of UTF-8 in IMAP protocol strings such as folder names. +

+Properties +

+The IMAP protocol provider supports the following properties, +which may be set in the Jakarta Mail Session object. +The properties are always set as strings; the Type column describes +how the string is interpreted. For example, use +

+
+	props.put("mail.imap.port", "888");
+
+

+to set the mail.imap.port property, which is of type int. +

+

+Note that if you're using the "imaps" protocol to access IMAP over SSL, +all the properties would be named "mail.imaps.*". +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IMAP properties
NameTypeDescription
mail.imap.userStringDefault user name for IMAP.
mail.imap.hostStringThe IMAP server to connect to.
mail.imap.portintThe IMAP server port to connect to, if the connect() method doesn't +explicitly specify one. Defaults to 143.
mail.imap.partialfetchbooleanControls whether the IMAP partial-fetch capability should be used. +Defaults to true.
mail.imap.fetchsizeintPartial fetch size in bytes. Defaults to 16K.
mail.imap.peekboolean +If set to true, use the IMAP PEEK option when fetching body parts, +to avoid setting the SEEN flag on messages. +Defaults to false. +Can be overridden on a per-message basis by the +{@link com.sun.mail.imap.IMAPMessage#setPeek setPeek} +method on IMAPMessage. +
mail.imap.ignorebodystructuresizebooleanThe IMAP BODYSTRUCTURE response includes the exact size of each body part. +Normally, this size is used to determine how much data to fetch for each +body part. +Some servers report this size incorrectly in some cases; this property can +be set to work around such server bugs. +If this property is set to true, this size is ignored and data is fetched +until the server reports the end of data. +This will result in an extra fetch if the data size is a multiple of the +block size. +Defaults to false.
mail.imap.connectiontimeoutintSocket connection timeout value in milliseconds. +This timeout is implemented by java.net.Socket. +Default is infinite timeout.
mail.imap.timeoutintSocket read timeout value in milliseconds. +This timeout is implemented by java.net.Socket. +Default is infinite timeout.
mail.imap.writetimeoutintSocket write timeout value in milliseconds. +This timeout is implemented by using a +java.util.concurrent.ScheduledExecutorService per connection +that schedules a thread to close the socket if the timeout expires. +Thus, the overhead of using this timeout is one thread per connection. +Default is infinite timeout.
mail.imap.statuscachetimeoutintTimeout value in milliseconds for cache of STATUS command response. +Default is 1000 (1 second). Zero disables cache.
mail.imap.appendbuffersizeint +Maximum size of a message to buffer in memory when appending to an IMAP +folder. If not set, or set to -1, there is no maximum and all messages +are buffered. If set to 0, no messages are buffered. If set to (e.g.) +8192, messages of 8K bytes or less are buffered, larger messages are +not buffered. Buffering saves cpu time at the expense of short term +memory usage. If you commonly append very large messages to IMAP +mailboxes you might want to set this to a moderate value (1M or less). +
mail.imap.connectionpoolsizeintMaximum number of available connections in the connection pool. +Default is 1.
mail.imap.connectionpooltimeoutintTimeout value in milliseconds for connection pool connections. Default +is 45000 (45 seconds).
mail.imap.separatestoreconnectionbooleanFlag to indicate whether to use a dedicated store connection for store +commands. Default is false.
mail.imap.allowreadonlyselectbooleanIf false, attempts to open a folder read/write will fail +if the SELECT command succeeds but indicates that the folder is READ-ONLY. +This sometimes indicates that the folder contents can'tbe changed, but +the flags are per-user and can be changed, such as might be the case for +public shared folders. If true, such open attempts will succeed, allowing +the flags to be changed. The getMode method on the +Folder object will return Folder.READ_ONLY +in this case even though the open method specified +Folder.READ_WRITE. Default is false.
mail.imap.auth.mechanismsString +If set, lists the authentication mechanisms to consider, and the order +in which to consider them. Only mechanisms supported by the server and +supported by the current implementation will be used. +The default is "PLAIN LOGIN NTLM", which includes all +the authentication mechanisms supported by the current implementation +except XOAUTH2. +
mail.imap.auth.login.disablebooleanIf true, prevents use of the non-standard AUTHENTICATE LOGIN +command, instead using the plain LOGIN command. +Default is false.
mail.imap.auth.plain.disablebooleanIf true, prevents use of the AUTHENTICATE PLAIN command. +Default is false.
mail.imap.auth.ntlm.disablebooleanIf true, prevents use of the AUTHENTICATE NTLM command. +Default is false.
mail.imap.auth.ntlm.domainString +The NTLM authentication domain. +
mail.imap.auth.ntlm.flagsint +NTLM protocol-specific flags. +See +http://curl.haxx.se/rfc/ntlm.html#theNtlmFlags for details. +
mail.imap.auth.xoauth2.disablebooleanIf true, prevents use of the AUTHENTICATE XOAUTH2 command. +Because the OAuth 2.0 protocol requires a special access token instead of +a password, this mechanism is disabled by default. Enable it by explicitly +setting this property to "false" or by setting the "mail.imap.auth.mechanisms" +property to "XOAUTH2".
mail.imap.proxyauth.userStringIf the server supports the PROXYAUTH extension, this property +specifies the name of the user to act as. Authenticate to the +server using the administrator's credentials. After authentication, +the IMAP provider will issue the PROXYAUTH command with +the user name specified in this property. +
mail.imap.localaddressString +Local address (host name) to bind to when creating the IMAP socket. +Defaults to the address picked by the Socket class. +Should not normally need to be set, but useful with multi-homed hosts +where it's important to pick a particular local address to bind to. +
mail.imap.localportint +Local port number to bind to when creating the IMAP socket. +Defaults to the port number picked by the Socket class. +
mail.imap.sasl.enableboolean +If set to true, attempt to use the javax.security.sasl package to +choose an authentication mechanism for login. +Defaults to false. +
mail.imap.sasl.mechanismsString +A space or comma separated list of SASL mechanism names to try +to use. +
mail.imap.sasl.authorizationidString +The authorization ID to use in the SASL authentication. +If not set, the authentication ID (user name) is used. +
mail.imap.sasl.realmStringThe realm to use with SASL authentication mechanisms that +require a realm, such as DIGEST-MD5.
mail.imap.sasl.usecanonicalhostnameboolean +If set to true, the canonical host name returned by +{@link java.net.InetAddress#getCanonicalHostName InetAddress.getCanonicalHostName} +is passed to the SASL mechanism, instead of the host name used to connect. +Defaults to false. +
mail.imap.sasl. xgwtrustedapphack.enableboolean +If set to true, enables a workaround for a bug in the Novell Groupwise +XGWTRUSTEDAPP SASL mechanism, when that mechanism is being used. +Defaults to true. +
mail.imap.socketFactorySocketFactory +If set to a class that implements the +javax.net.SocketFactory interface, this class +will be used to create IMAP sockets. Note that this is an +instance of a class, not a name, and must be set using the +put method, not the setProperty method. +
mail.imap.socketFactory.classString +If set, specifies the name of a class that implements the +javax.net.SocketFactory interface. This class +will be used to create IMAP sockets. +
mail.imap.socketFactory.fallbackboolean +If set to true, failure to create a socket using the specified +socket factory class will cause the socket to be created using +the java.net.Socket class. +Defaults to true. +
mail.imap.socketFactory.portint +Specifies the port to connect to when using the specified socket +factory. +If not set, the default port will be used. +
mail.imap.usesocketchannelsboolean +If set to true, use SocketChannels instead of Sockets for connecting +to the server. Required if using the IdleManager. +Ignored if a socket factory is set. +Defaults to false. +
mail.imap.ssl.enableboolean +If set to true, use SSL to connect and use the SSL port by default. +Defaults to false for the "imap" protocol and true for the "imaps" protocol. +
mail.imap.ssl.checkserveridentityboolean +If set to true, check the server identity as specified by +RFC 2595. +These additional checks based on the content of the server's certificate +are intended to prevent man-in-the-middle attacks. +Defaults to false. +
mail.imap.ssl.trustString +If set, and a socket factory hasn't been specified, enables use of a +{@link com.sun.mail.util.MailSSLSocketFactory MailSSLSocketFactory}. +If set to "*", all hosts are trusted. +If set to a whitespace separated list of hosts, those hosts are trusted. +Otherwise, trust depends on the certificate the server presents. +
mail.imap.ssl.socketFactorySSLSocketFactory +If set to a class that extends the +javax.net.ssl.SSLSocketFactory class, this class +will be used to create IMAP SSL sockets. Note that this is an +instance of a class, not a name, and must be set using the +put method, not the setProperty method. +
mail.imap.ssl.socketFactory.classString +If set, specifies the name of a class that extends the +javax.net.ssl.SSLSocketFactory class. This class +will be used to create IMAP SSL sockets. +
mail.imap.ssl.socketFactory.portint +Specifies the port to connect to when using the specified socket +factory. +If not set, the default port will be used. +
mail.imap.ssl.protocolsstring +Specifies the SSL protocols that will be enabled for SSL connections. +The property value is a whitespace separated list of tokens acceptable +to the javax.net.ssl.SSLSocket.setEnabledProtocols method. +
mail.imap.ssl.ciphersuitesstring +Specifies the SSL cipher suites that will be enabled for SSL connections. +The property value is a whitespace separated list of tokens acceptable +to the javax.net.ssl.SSLSocket.setEnabledCipherSuites method. +
mail.imap.starttls.enablebooleanIf true, enables the use of the STARTTLS command (if +supported by the server) to switch the connection to a TLS-protected +connection before issuing any login commands. +If the server does not support STARTTLS, the connection continues without +the use of TLS; see the +mail.imap.starttls.required +property to fail if STARTTLS isn't supported. +Note that an appropriate trust store must configured so that the client +will trust the server's certificate. +Default is false.
mail.imap.starttls.requiredboolean +If true, requires the use of the STARTTLS command. +If the server doesn't support the STARTTLS command, or the command +fails, the connect method will fail. +Defaults to false. +
mail.imap.proxy.hoststring +Specifies the host name of an HTTP web proxy server that will be used for +connections to the mail server. +
mail.imap.proxy.portstring +Specifies the port number for the HTTP web proxy server. +Defaults to port 80. +
mail.imap.proxy.userstring +Specifies the user name to use to authenticate with the HTTP web proxy server. +By default, no authentication is done. +
mail.imap.proxy.passwordstring +Specifies the password to use to authenticate with the HTTP web proxy server. +By default, no authentication is done. +
mail.imap.socks.hoststring +Specifies the host name of a SOCKS5 proxy server that will be used for +connections to the mail server. +
mail.imap.socks.portstring +Specifies the port number for the SOCKS5 proxy server. +This should only need to be used if the proxy server is not using +the standard port number of 1080. +
mail.imap.minidletimeint +Applications typically call the idle method in a loop. If another +thread termiantes the IDLE command, it needs a chance to do its +work before another IDLE command is issued. The idle method enforces +a delay to prevent thrashing between the IDLE command and regular +commands. This property sets the delay in milliseconds. If not +set, the default is 10 milliseconds. +
mail.imap.enableresponseeventsboolean +Enable special IMAP-specific events to be delivered to the Store's +ConnectionListener. If true, IMAP OK, NO, BAD, or BYE responses +will be sent as ConnectionEvents with a type of +IMAPStore.RESPONSE. The event's message will be the +raw IMAP response string. +By default, these events are not sent. +NOTE: This capability is highly experimental and likely will change +in future releases. +
mail.imap.enableimapeventsboolean +Enable special IMAP-specific events to be delivered to the Store's +ConnectionListener. If true, unsolicited responses +received during the Store's idle method will be sent +as ConnectionEvents with a type of +IMAPStore.RESPONSE. The event's message will be the +raw IMAP response string. +By default, these events are not sent. +NOTE: This capability is highly experimental and likely will change +in future releases. +
mail.imap.throwsearchexceptionboolean +If set to true and a {@link javax.mail.search.SearchTerm SearchTerm} +passed to the +{@link javax.mail.Folder#search Folder.search} +method is too complex for the IMAP protocol, throw a +{@link javax.mail.search.SearchException SearchException}. +For example, the IMAP protocol only supports less-than and greater-than +comparisons for a {@link javax.mail.search.SizeTerm SizeTerm}. +If false, the search will be done locally by fetching the required +message data and comparing it locally. +Defaults to false. +
mail.imap.folder.classString +Class name of a subclass of com.sun.mail.imap.IMAPFolder. +The subclass can be used to provide support for additional IMAP commands. +The subclass must have public constructors of the form +public MyIMAPFolder(String fullName, char separator, IMAPStore store, +Boolean isNamespace) and +public MyIMAPFolder(ListInfo li, IMAPStore store) +
mail.imap.closefoldersonstorefailureboolean +In some cases, a failure of the Store connection indicates a failure of the +server, and all Folders associated with that Store should also be closed. +In other cases, a Store connection failure may be a transient failure, and +Folders may continue to operate normally. +If this property is true (the default), failures in the Store connection cause +all associated Folders to be closed. +Set this property to false to better handle transient failures in the Store +connection. +
mail.imap.finalizecleancloseboolean +When the finalizer for IMAPStore is called, +should the connection to the server be closed cleanly, as if the +application called the close method? +Or should the connection to the server be closed without sending +any commands to the server? +Defaults to false, the connection is not closed cleanly. +
mail.imap.referralexceptionboolean +If set to true and an IMAP login referral is returned when the authentication +succeeds, fail the connect request and throw a +{@link com.sun.mail.imap.ReferralException ReferralException}. +Defaults to false. +
mail.imap.compress.enableboolean +If set to true and the IMAP server supports the COMPRESS=DEFLATE extension, +compression will be enabled. +Defaults to false. +
mail.imap.compress.levelint +The compression level to be used, in the range -1 to 9. +See the {@link java.util.zip.Deflater Deflater} class for details. +
mail.imap.compress.strategyint +The compression strategy to be used, in the range 0 to 2. +See the {@link java.util.zip.Deflater Deflater} class for details. +
mail.imap.reusetagprefixboolean +If true, always use "A" for the IMAP command tag prefix. +If false, the IMAP command tag prefix is different for each connection, +from "A" through "ZZZ" and then wrapping around to "A". +Applications should never need to set this. +Defaults to false. +
+

+In general, applications should not need to use the classes in this +package directly. Instead, they should use the APIs defined by the +javax.mail package (and subpackages). Applications should +never construct instances of IMAPStore or +IMAPFolder directly. Instead, they should use the +Session method getStore to acquire an +appropriate Store object, and from that acquire +Folder objects. +

+Loggers +

+In addition to printing debugging output as controlled by the +{@link javax.mail.Session Session} configuration, +the com.sun.mail.imap provider logs the same information using +{@link java.util.logging.Logger} as described in the following table: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IMAP Loggers
Logger NameLogging LevelPurpose
com.sun.mail.imapCONFIGConfiguration of the IMAPStore
com.sun.mail.imapFINEGeneral debugging output
com.sun.mail.imap.connectionpoolCONFIGConfiguration of the IMAP connection pool
com.sun.mail.imap.connectionpoolFINEDebugging output related to the IMAP connection pool
com.sun.mail.imap.messagecacheCONFIGConfiguration of the IMAP message cache
com.sun.mail.imap.messagecacheFINEDebugging output related to the IMAP message cache
com.sun.mail.imap.protocolFINESTComplete protocol trace
+ +WARNING +

+WARNING: The APIs unique to this package should be +considered EXPERIMENTAL. They may be changed in the +future in ways that are incompatible with applications using the +current APIs. +

+ + + diff --git a/app/src/main/java/com/sun/mail/imap/protocol/BASE64MailboxDecoder.java b/app/src/main/java/com/sun/mail/imap/protocol/BASE64MailboxDecoder.java new file mode 100644 index 0000000000..af35020e9c --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/BASE64MailboxDecoder.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.text.StringCharacterIterator; +import java.text.CharacterIterator; + +/** + * See the BASE64MailboxEncoder for a description of the RFC2060 and how + * mailbox names should be encoded. This class will do the correct decoding + * for mailbox names. + * + * @author Christopher Cotton + */ + +public class BASE64MailboxDecoder { + + public static String decode(String original) { + if (original == null || original.length() == 0) + return original; + + boolean changedString = false; + int copyTo = 0; + // it will always be less than the original + char[] chars = new char[original.length()]; + StringCharacterIterator iter = new StringCharacterIterator(original); + + for(char c = iter.first(); c != CharacterIterator.DONE; + c = iter.next()) { + + if (c == '&') { + changedString = true; + copyTo = base64decode(chars, copyTo, iter); + } else { + chars[copyTo++] = c; + } + } + + // now create our string from the char array + if (changedString) { + return new String(chars, 0, copyTo); + } else { + return original; + } + } + + + protected static int base64decode(char[] buffer, int offset, + CharacterIterator iter) { + boolean firsttime = true; + int leftover = -1; + + while(true) { + // get the first byte + byte orig_0 = (byte) iter.next(); + if (orig_0 == -1) break; // no more chars + if (orig_0 == '-') { + if (firsttime) { + // means we got the string "&-" which is turned into a "&" + buffer[offset++] = '&'; + } + // we are done now + break; + } + firsttime = false; + + // next byte + byte orig_1 = (byte) iter.next(); + if (orig_1 == -1 || orig_1 == '-') + break; // no more chars, invalid base64 + + byte a, b, current; + a = pem_convert_array[orig_0 & 0xff]; + b = pem_convert_array[orig_1 & 0xff]; + // The first decoded byte + current = (byte)(((a << 2) & 0xfc) | ((b >>> 4) & 3)); + + // use the leftover to create a Unicode Character (2 bytes) + if (leftover != -1) { + buffer[offset++] = (char)(leftover << 8 | (current & 0xff)); + leftover = -1; + } else { + leftover = current & 0xff; + } + + byte orig_2 = (byte) iter.next(); + if (orig_2 == '=') { // End of this BASE64 encoding + continue; + } else if (orig_2 == -1 || orig_2 == '-') { + break; // no more chars + } + + // second decoded byte + a = b; + b = pem_convert_array[orig_2 & 0xff]; + current = (byte)(((a << 4) & 0xf0) | ((b >>> 2) & 0xf)); + + // use the leftover to create a Unicode Character (2 bytes) + if (leftover != -1) { + buffer[offset++] = (char)(leftover << 8 | (current & 0xff)); + leftover = -1; + } else { + leftover = current & 0xff; + } + + byte orig_3 = (byte) iter.next(); + if (orig_3 == '=') { // End of this BASE64 encoding + continue; + } else if (orig_3 == -1 || orig_3 == '-') { + break; // no more chars + } + + // The third decoded byte + a = b; + b = pem_convert_array[orig_3 & 0xff]; + current = (byte)(((a << 6) & 0xc0) | (b & 0x3f)); + + // use the leftover to create a Unicode Character (2 bytes) + if (leftover != -1) { + buffer[offset++] = (char)(leftover << 8 | (current & 0xff)); + leftover = -1; + } else { + leftover = current & 0xff; + } + } + + return offset; + } + + /** + * This character array provides the character to value map + * based on RFC1521, but with the modification from RFC2060 + * which changes the '/' to a ','. + */ + + // shared with BASE64MailboxEncoder + static final char pem_array[] = { + 'A','B','C','D','E','F','G','H', // 0 + 'I','J','K','L','M','N','O','P', // 1 + 'Q','R','S','T','U','V','W','X', // 2 + 'Y','Z','a','b','c','d','e','f', // 3 + 'g','h','i','j','k','l','m','n', // 4 + 'o','p','q','r','s','t','u','v', // 5 + 'w','x','y','z','0','1','2','3', // 6 + '4','5','6','7','8','9','+',',' // 7 + }; + + private static final byte pem_convert_array[] = new byte[256]; + + static { + for (int i = 0; i < 255; i++) + pem_convert_array[i] = -1; + for(int i = 0; i < pem_array.length; i++) + pem_convert_array[pem_array[i]] = (byte) i; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/BASE64MailboxEncoder.java b/app/src/main/java/com/sun/mail/imap/protocol/BASE64MailboxEncoder.java new file mode 100644 index 0000000000..1013baf021 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/BASE64MailboxEncoder.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.io.*; + + +/** + * From RFC2060: + * + *
+ *
+ * 5.1.3.  Mailbox International Naming Convention
+ *
+ *   By convention, international mailbox names are specified using a
+ *   modified version of the UTF-7 encoding described in [UTF-7].  The
+ *   purpose of these modifications is to correct the following problems
+ *   with UTF-7:
+ *
+ *      1) UTF-7 uses the "+" character for shifting; this conflicts with
+ *         the common use of "+" in mailbox names, in particular USENET
+ *         newsgroup names.
+ *
+ *      2) UTF-7's encoding is BASE64 which uses the "/" character; this
+ *         conflicts with the use of "/" as a popular hierarchy delimiter.
+ *
+ *      3) UTF-7 prohibits the unencoded usage of "\"; this conflicts with
+ *         the use of "\" as a popular hierarchy delimiter.
+ *
+ *      4) UTF-7 prohibits the unencoded usage of "~"; this conflicts with
+ *         the use of "~" in some servers as a home directory indicator.
+ *
+ *      5) UTF-7 permits multiple alternate forms to represent the same
+ *         string; in particular, printable US-ASCII chararacters can be
+ *         represented in encoded form.
+ *
+ *   In modified UTF-7, printable US-ASCII characters except for "&"
+ *   represent themselves; that is, characters with octet values 0x20-0x25
+ *   and 0x27-0x7e.  The character "&" (0x26) is represented by the two-
+ *   octet sequence "&-".
+ *
+ *   All other characters (octet values 0x00-0x1f, 0x7f-0xff, and all
+ *   Unicode 16-bit octets) are represented in modified BASE64, with a
+ *   further modification from [UTF-7] that "," is used instead of "/".
+ *   Modified BASE64 MUST NOT be used to represent any printing US-ASCII
+ *   character which can represent itself.
+ *
+ *   "&" is used to shift to modified BASE64 and "-" to shift back to US-
+ *   ASCII.  All names start in US-ASCII, and MUST end in US-ASCII (that
+ *   is, a name that ends with a Unicode 16-bit octet MUST end with a "-
+ *   ").
+ *
+ *   For example, here is a mailbox name which mixes English, Japanese,
+ *   and Chinese text: ~peter/mail/&ZeVnLIqe-/&U,BTFw-
+ *
+ * 
+ * + * This class will do the correct Encoding for the IMAP mailboxes. + * + * @author Christopher Cotton + */ + +public class BASE64MailboxEncoder { + protected byte[] buffer = new byte[4]; + protected int bufsize = 0; + protected boolean started = false; + protected Writer out = null; + + + public static String encode(String original) { + BASE64MailboxEncoder base64stream = null; + char origchars[] = original.toCharArray(); + int length = origchars.length; + boolean changedString = false; + CharArrayWriter writer = new CharArrayWriter(length); + + // loop over all the chars + for(int index = 0; index < length; index++) { + char current = origchars[index]; + + // octets in the range 0x20-0x25,0x27-0x7e are themselves + // 0x26 "&" is represented as "&-" + if (current >= 0x20 && current <= 0x7e) { + if (base64stream != null) { + base64stream.flush(); + } + + if (current == '&') { + changedString = true; + writer.write('&'); + writer.write('-'); + } else { + writer.write(current); + } + } else { + + // use a B64MailboxEncoder to write out the other bytes + // as a modified BASE64. The stream will write out + // the beginning '&' and the ending '-' which is part + // of every encoding. + + if (base64stream == null) { + base64stream = new BASE64MailboxEncoder(writer); + changedString = true; + } + + base64stream.write(current); + } + } + + + if (base64stream != null) { + base64stream.flush(); + } + + if (changedString) { + return writer.toString(); + } else { + return original; + } + } + + + /** + * Create a BASE64 encoder + * + * @param what where to write the encoded name + */ + public BASE64MailboxEncoder(Writer what) { + out = what; + } + + public void write(int c) { + try { + // write out the initial character if this is the first time + if (!started) { + started = true; + out.write('&'); + } + + // we write each character as a 2 byte unicode character + buffer[bufsize++] = (byte) (c >> 8); + buffer[bufsize++] = (byte) (c & 0xff); + + if (bufsize >= 3) { + encode(); + bufsize -= 3; + } + } catch (IOException e) { + //e.printStackTrace(); + } + } + + + public void flush() { + try { + // flush any bytes we have + if (bufsize > 0) { + encode(); + bufsize = 0; + } + + // write the terminating character of the encoding + if (started) { + out.write('-'); + started = false; + } + } catch (IOException e) { + //e.printStackTrace(); + } + } + + + protected void encode() throws IOException { + byte a, b, c; + if (bufsize == 1) { + a = buffer[0]; + b = 0; + c = 0; + out.write(pem_array[(a >>> 2) & 0x3F]); + out.write(pem_array[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + // no padding characters are written + } else if (bufsize == 2) { + a = buffer[0]; + b = buffer[1]; + c = 0; + out.write(pem_array[(a >>> 2) & 0x3F]); + out.write(pem_array[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + out.write(pem_array[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); + // no padding characters are written + } else { + a = buffer[0]; + b = buffer[1]; + c = buffer[2]; + out.write(pem_array[(a >>> 2) & 0x3F]); + out.write(pem_array[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + out.write(pem_array[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); + out.write(pem_array[c & 0x3F]); + + // copy back the extra byte + if (bufsize == 4) + buffer[0] = buffer[3]; + } + } + + private final static char pem_array[] = { + 'A','B','C','D','E','F','G','H', // 0 + 'I','J','K','L','M','N','O','P', // 1 + 'Q','R','S','T','U','V','W','X', // 2 + 'Y','Z','a','b','c','d','e','f', // 3 + 'g','h','i','j','k','l','m','n', // 4 + 'o','p','q','r','s','t','u','v', // 5 + 'w','x','y','z','0','1','2','3', // 6 + '4','5','6','7','8','9','+',',' // 7 + }; +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/BODY.java b/app/src/main/java/com/sun/mail/imap/protocol/BODY.java new file mode 100644 index 0000000000..5f04edc2c5 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/BODY.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.io.ByteArrayInputStream; +import com.sun.mail.iap.*; +import com.sun.mail.util.ASCIIUtility; + +/** + * The BODY fetch response item. + * + * @author John Mani + * @author Bill Shannon + */ + +public class BODY implements Item { + + static final char[] name = {'B','O','D','Y'}; + + private final int msgno; + private final ByteArray data; + private final String section; + private final int origin; + private final boolean isHeader; + + /** + * Constructor + * + * @param r the FetchResponse + * @exception ParsingException for parsing failures + */ + public BODY(FetchResponse r) throws ParsingException { + msgno = r.getNumber(); + + r.skipSpaces(); + + if (r.readByte() != '[') + throw new ParsingException( + "BODY parse error: missing ``['' at section start"); + section = r.readString(']'); + if (r.readByte() != ']') + throw new ParsingException( + "BODY parse error: missing ``]'' at section end"); + isHeader = section.regionMatches(true, 0, "HEADER", 0, 6); + + if (r.readByte() == '<') { // origin + origin = r.readNumber(); + r.skip(1); // skip '>'; + } else + origin = -1; + + data = r.readByteArray(); + } + + public ByteArray getByteArray() { + return data; + } + + public ByteArrayInputStream getByteArrayInputStream() { + if (data != null) + return data.toByteArrayInputStream(); + else + return null; + } + + public boolean isHeader() { + return isHeader; + } + + public String getSection() { + return section; + } + + /** + * @since Jakarta Mail 1.6.4 + */ + public int getOrigin() { + return origin; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/BODYSTRUCTURE.java b/app/src/main/java/com/sun/mail/imap/protocol/BODYSTRUCTURE.java new file mode 100644 index 0000000000..3a5945cc0e --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/BODYSTRUCTURE.java @@ -0,0 +1,450 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.List; +import java.util.ArrayList; +import javax.mail.internet.ParameterList; +import com.sun.mail.iap.*; +import com.sun.mail.util.PropUtil; + +/** + * A BODYSTRUCTURE response. + * + * @author John Mani + * @author Bill Shannon + */ + +public class BODYSTRUCTURE implements Item { + + static final char[] name = + {'B','O','D','Y','S','T','R','U','C','T','U','R','E'}; + public int msgno; + + public String type; // Type + public String subtype; // Subtype + public String encoding; // Encoding + public int lines = -1; // Size in lines + public int size = -1; // Size in bytes + public String disposition; // Disposition + public String id; // Content-ID + public String description; // Content-Description + public String md5; // MD-5 checksum + public String attachment; // Attachment name + public ParameterList cParams; // Body parameters + public ParameterList dParams; // Disposition parameters + public String[] language; // Language + public BODYSTRUCTURE[] bodies; // array of BODYSTRUCTURE objects + // for multipart & message/rfc822 + public ENVELOPE envelope; // for message/rfc822 + + private static int SINGLE = 1; + private static int MULTI = 2; + private static int NESTED = 3; + private int processedType; // MULTI | SINGLE | NESTED + + // special debugging output to debug parsing errors + private static final boolean parseDebug = + PropUtil.getBooleanSystemProperty("mail.imap.parse.debug", false); + + + public BODYSTRUCTURE(FetchResponse r) throws ParsingException { + if (parseDebug) + System.out.println("DEBUG IMAP: parsing BODYSTRUCTURE"); + msgno = r.getNumber(); + if (parseDebug) + System.out.println("DEBUG IMAP: msgno " + msgno); + + r.skipSpaces(); + + if (r.readByte() != '(') + throw new ParsingException( + "BODYSTRUCTURE parse error: missing ``('' at start"); + + if (r.peekByte() == '(') { // multipart + if (parseDebug) + System.out.println("DEBUG IMAP: parsing multipart"); + type = "multipart"; + processedType = MULTI; + List v = new ArrayList<>(1); + int i = 1; + do { + v.add(new BODYSTRUCTURE(r)); + /* + * Even though the IMAP spec says there can't be any spaces + * between parts, some servers erroneously put a space in + * here. In the spirit of "be liberal in what you accept", + * we skip it. + */ + r.skipSpaces(); + } while (r.peekByte() == '('); + + // setup bodies. + bodies = v.toArray(new BODYSTRUCTURE[v.size()]); + + subtype = r.readString(); // subtype + if (parseDebug) + System.out.println("DEBUG IMAP: subtype " + subtype); + + if (r.isNextNonSpace(')')) { // done + if (parseDebug) + System.out.println("DEBUG IMAP: parse DONE"); + return; + } + + // Else, we have extension data + + if (parseDebug) + System.out.println("DEBUG IMAP: parsing extension data"); + // Body parameters + cParams = parseParameters(r); + if (r.isNextNonSpace(')')) { // done + if (parseDebug) + System.out.println("DEBUG IMAP: body parameters DONE"); + return; + } + + // Disposition + byte b = r.peekByte(); + if (b == '(') { + if (parseDebug) + System.out.println("DEBUG IMAP: parse disposition"); + r.readByte(); + disposition = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: disposition " + + disposition); + dParams = parseParameters(r); + if (!r.isNextNonSpace(')')) // eat the end ')' + throw new ParsingException( + "BODYSTRUCTURE parse error: " + + "missing ``)'' at end of disposition in multipart"); + if (parseDebug) + System.out.println("DEBUG IMAP: disposition DONE"); + } else if (b == 'N' || b == 'n') { + if (parseDebug) + System.out.println("DEBUG IMAP: disposition NIL"); + r.skip(3); // skip 'NIL' + } else { + /* + throw new ParsingException( + "BODYSTRUCTURE parse error: " + + type + "/" + subtype + ": " + + "bad multipart disposition, b " + b); + */ + if (parseDebug) + System.out.println("DEBUG IMAP: bad multipart disposition" + + ", applying Exchange bug workaround"); + description = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: multipart description " + + description); + // Throw away whatever comes after it, since we have no + // idea what it's supposed to be + while (r.readByte() == ' ') + parseBodyExtension(r); + return; + } + + // RFC3501 allows no body-fld-lang after body-fld-disp, + // even though RFC2060 required it + if (r.isNextNonSpace(')')) { + if (parseDebug) + System.out.println("DEBUG IMAP: no body-fld-lang"); + return; // done + } + + // Language + if (r.peekByte() == '(') { // a list follows + language = r.readStringList(); + if (parseDebug) + System.out.println( + "DEBUG IMAP: language len " + language.length); + } else { + String l = r.readString(); + if (l != null) { + String[] la = { l }; + language = la; + if (parseDebug) + System.out.println("DEBUG IMAP: language " + l); + } + } + + // RFC3501 defines an optional "body location" next, + // but for now we ignore it along with other extensions. + + // Throw away any further extension data + while (r.readByte() == ' ') + parseBodyExtension(r); + } else if (r.peekByte() == ')') { // (illegal) empty body + /* + * Domino will fail to return the body structure of nested messages. + * Fake it by providing an empty message. Could probably do better + * with more work... + */ + /* + * XXX - this prevents the exception, but without the exception + * the application has no way to know the data from the message + * is missing. + * + if (parseDebug) + System.out.println("DEBUG IMAP: empty body, fake it"); + r.readByte(); + type = "text"; + subtype = "plain"; + lines = 0; + size = 0; + */ + throw new ParsingException( + "BODYSTRUCTURE parse error: missing body content"); + } else { // Single part + if (parseDebug) + System.out.println("DEBUG IMAP: single part"); + type = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: type " + type); + processedType = SINGLE; + subtype = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: subtype " + subtype); + + // SIMS 4.0 returns NIL for a Content-Type of "binary", fix it here + if (type == null) { + type = "application"; + subtype = "octet-stream"; + } + cParams = parseParameters(r); + if (parseDebug) + System.out.println("DEBUG IMAP: cParams " + cParams); + id = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: id " + id); + description = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: description " + description); + /* + * XXX - Work around bug in Exchange 2010 that + * returns unquoted string. + */ + encoding = r.readAtomString(); + if (encoding != null && encoding.equalsIgnoreCase("NIL")) { + if (parseDebug) + System.out.println("DEBUG IMAP: NIL encoding" + + ", applying Exchange bug workaround"); + encoding = null; + } + /* + * XXX - Work around bug in office365.com that returns + * a string with a trailing space in some cases. + */ + if (encoding != null) + encoding = encoding.trim(); + if (parseDebug) + System.out.println("DEBUG IMAP: encoding " + encoding); + size = r.readNumber(); + if (parseDebug) + System.out.println("DEBUG IMAP: size " + size); + if (size < 0) + throw new ParsingException( + "BODYSTRUCTURE parse error: bad ``size'' element"); + + // "text/*" & "message/rfc822" types have additional data .. + if (type.equalsIgnoreCase("text")) { + lines = r.readNumber(); + if (parseDebug) + System.out.println("DEBUG IMAP: lines " + lines); + if (lines < 0) + throw new ParsingException( + "BODYSTRUCTURE parse error: bad ``lines'' element"); + } else if (type.equalsIgnoreCase("message") && + subtype.equalsIgnoreCase("rfc822")) { + // Nested message + processedType = NESTED; + // The envelope comes next, but sadly Gmail handles nested + // messages just like simple body parts and fails to return + // the envelope and body structure of the message (sort of + // like IMAP4 before rev1). + r.skipSpaces(); + if (r.peekByte() == '(') { // the envelope follows + envelope = new ENVELOPE(r); + if (parseDebug) + System.out.println( + "DEBUG IMAP: got envelope of nested message"); + BODYSTRUCTURE[] bs = { new BODYSTRUCTURE(r) }; + bodies = bs; + lines = r.readNumber(); + if (parseDebug) + System.out.println("DEBUG IMAP: lines " + lines); + if (lines < 0) + throw new ParsingException( + "BODYSTRUCTURE parse error: bad ``lines'' element"); + } else { + if (parseDebug) + System.out.println("DEBUG IMAP: " + + "missing envelope and body of nested message"); + } + } else { + // Detect common error of including lines element on other types + r.skipSpaces(); + byte bn = r.peekByte(); + if (Character.isDigit((char)bn)) // number + throw new ParsingException( + "BODYSTRUCTURE parse error: server erroneously " + + "included ``lines'' element with type " + + type + "/" + subtype); + } + + if (r.isNextNonSpace(')')) { + if (parseDebug) + System.out.println("DEBUG IMAP: parse DONE"); + return; // done + } + + // Optional extension data + + // MD5 + md5 = r.readString(); + if (r.isNextNonSpace(')')) { + if (parseDebug) + System.out.println("DEBUG IMAP: no MD5 DONE"); + return; // done + } + + // Disposition + byte b = r.readByte(); + if (b == '(') { + disposition = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: disposition " + + disposition); + dParams = parseParameters(r); + if (parseDebug) + System.out.println("DEBUG IMAP: dParams " + dParams); + if (!r.isNextNonSpace(')')) // eat the end ')' + throw new ParsingException( + "BODYSTRUCTURE parse error: " + + "missing ``)'' at end of disposition"); + } else if (b == 'N' || b == 'n') { + if (parseDebug) + System.out.println("DEBUG IMAP: disposition NIL"); + r.skip(2); // skip 'NIL' + } else { + throw new ParsingException( + "BODYSTRUCTURE parse error: " + + type + "/" + subtype + ": " + + "bad single part disposition, b " + b); + } + + if (r.isNextNonSpace(')')) { + if (parseDebug) + System.out.println("DEBUG IMAP: disposition DONE"); + return; // done + } + + // Language + if (r.peekByte() == '(') { // a list follows + language = r.readStringList(); + if (parseDebug) + System.out.println("DEBUG IMAP: language len " + + language.length); + } else { // protocol is unnessarily complex here + String l = r.readString(); + if (l != null) { + String[] la = { l }; + language = la; + if (parseDebug) + System.out.println("DEBUG IMAP: language " + l); + } + } + + // RFC3501 defines an optional "body location" next, + // but for now we ignore it along with other extensions. + + // Throw away any further extension data + while (r.readByte() == ' ') + parseBodyExtension(r); + if (parseDebug) + System.out.println("DEBUG IMAP: all DONE"); + } + } + + public boolean isMulti() { + return processedType == MULTI; + } + + public boolean isSingle() { + return processedType == SINGLE; + } + + public boolean isNested() { + return processedType == NESTED; + } + + private ParameterList parseParameters(Response r) + throws ParsingException { + r.skipSpaces(); + + ParameterList list = null; + byte b = r.readByte(); + if (b == '(') { + list = new ParameterList(); + do { + String name = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: parameter name " + name); + if (name == null) + throw new ParsingException( + "BODYSTRUCTURE parse error: " + + type + "/" + subtype + ": " + + "null name in parameter list"); + String value = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: parameter value " + value); + if (value == null) { // work around buggy servers + if (parseDebug) + System.out.println("DEBUG IMAP: NIL parameter value" + + ", applying Exchange bug workaround"); + value = ""; + } + list.set(name, value); + } while (!r.isNextNonSpace(')')); + list.combineSegments(); + } else if (b == 'N' || b == 'n') { + if (parseDebug) + System.out.println("DEBUG IMAP: parameter list NIL"); + r.skip(2); + } else + throw new ParsingException("Parameter list parse error"); + + return list; + } + + private void parseBodyExtension(Response r) throws ParsingException { + r.skipSpaces(); + + byte b = r.peekByte(); + if (b == '(') { + r.skip(1); // skip '(' + do { + parseBodyExtension(r); + } while (!r.isNextNonSpace(')')); + } else if (Character.isDigit((char)b)) // number + r.readNumber(); + else // nstring + r.readString(); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/ENVELOPE.java b/app/src/main/java/com/sun/mail/imap/protocol/ENVELOPE.java new file mode 100644 index 0000000000..eeafd143e2 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/ENVELOPE.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.List; +import java.util.ArrayList; +import java.util.Date; +import java.io.UnsupportedEncodingException; +import java.text.ParseException; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.AddressException; +import javax.mail.internet.MailDateFormat; +import javax.mail.internet.MimeUtility; +import com.sun.mail.iap.*; +import com.sun.mail.util.PropUtil; + +/** + * The ENEVELOPE item of an IMAP FETCH response. + * + * @author John Mani + * @author Bill Shannon + */ + +public class ENVELOPE implements Item { + + // IMAP item name + static final char[] name = {'E','N','V','E','L','O','P','E'}; + public int msgno; + + public Date date = null; + public String subject; + public InternetAddress[] from; + public InternetAddress[] sender; + public InternetAddress[] replyTo; + public InternetAddress[] to; + public InternetAddress[] cc; + public InternetAddress[] bcc; + public String inReplyTo; + public String messageId; + + // Used to parse dates + private static final MailDateFormat mailDateFormat = new MailDateFormat(); + + // special debugging output to debug parsing errors + private static final boolean parseDebug = + PropUtil.getBooleanSystemProperty("mail.imap.parse.debug", false); + + public ENVELOPE(FetchResponse r) throws ParsingException { + if (parseDebug) + System.out.println("parse ENVELOPE"); + msgno = r.getNumber(); + + r.skipSpaces(); + + if (r.readByte() != '(') + throw new ParsingException("ENVELOPE parse error"); + + String s = r.readString(); + if (s != null) { + try { + synchronized (mailDateFormat) { + date = mailDateFormat.parse(s); + } + } catch (ParseException pex) { + } + } + if (parseDebug) + System.out.println(" Date: " + date); + + subject = r.readString(); + if (parseDebug) + System.out.println(" Subject: " + subject); + if (parseDebug) + System.out.println(" From addresses:"); + from = parseAddressList(r); + if (parseDebug) + System.out.println(" Sender addresses:"); + sender = parseAddressList(r); + if (parseDebug) + System.out.println(" Reply-To addresses:"); + replyTo = parseAddressList(r); + if (parseDebug) + System.out.println(" To addresses:"); + to = parseAddressList(r); + if (parseDebug) + System.out.println(" Cc addresses:"); + cc = parseAddressList(r); + if (parseDebug) + System.out.println(" Bcc addresses:"); + bcc = parseAddressList(r); + inReplyTo = r.readString(); + if (parseDebug) + System.out.println(" In-Reply-To: " + inReplyTo); + messageId = r.readString(); + if (parseDebug) + System.out.println(" Message-ID: " + messageId); + + if (!r.isNextNonSpace(')')) + throw new ParsingException("ENVELOPE parse error"); + } + + private InternetAddress[] parseAddressList(Response r) + throws ParsingException { + r.skipSpaces(); // skip leading spaces + + byte b = r.readByte(); + if (b == '(') { + /* + * Some broken servers (e.g., Yahoo Mail) return an empty + * list instead of NIL. Handle that here even though it + * doesn't conform to the IMAP spec. + */ + if (r.isNextNonSpace(')')) + return null; + + List v = new ArrayList<>(); + + do { + IMAPAddress a = new IMAPAddress(r); + if (parseDebug) + System.out.println(" Address: " + a); + // if we see an end-of-group address at the top, ignore it + if (!a.isEndOfGroup()) + v.add(a); + } while (!r.isNextNonSpace(')')); + + return v.toArray(new InternetAddress[v.size()]); + } else if (b == 'N' || b == 'n') { // NIL + r.skip(2); // skip 'NIL' + return null; + } else + throw new ParsingException("ADDRESS parse error"); + } +} + +class IMAPAddress extends InternetAddress { + private boolean group = false; + private InternetAddress[] grouplist; + private String groupname; + + private static final long serialVersionUID = -3835822029483122232L; + + IMAPAddress(Response r) throws ParsingException { + r.skipSpaces(); // skip leading spaces + + if (r.readByte() != '(') + throw new ParsingException("ADDRESS parse error"); + + encodedPersonal = r.readString(); + + r.readString(); // throw away address_list + String mb = r.readString(); + String host = r.readString(); + // skip bogus spaces inserted by Yahoo IMAP server if + // "undisclosed-recipients" is a recipient + r.skipSpaces(); + if (!r.isNextNonSpace(')')) // skip past terminating ')' + throw new ParsingException("ADDRESS parse error"); + + if (host == null) { + // it's a group list, start or end + group = true; + groupname = mb; + if (groupname == null) // end of group list + return; + // Accumulate a group list. The members of the group + // are accumulated in a List and the corresponding string + // representation of the group is accumulated in a StringBuilder. + StringBuilder sb = new StringBuilder(); + sb.append(groupname).append(':'); + List v = new ArrayList<>(); + while (r.peekByte() != ')') { + IMAPAddress a = new IMAPAddress(r); + if (a.isEndOfGroup()) // reached end of group + break; + if (v.size() != 0) // if not first element, need a comma + sb.append(','); + sb.append(a.toString()); + v.add(a); + } + sb.append(';'); + address = sb.toString(); + grouplist = v.toArray(new IMAPAddress[v.size()]); + } else { + if (mb == null || mb.length() == 0) + address = host; + else if (host.length() == 0) + address = mb; + else + address = mb + "@" + host; + } + + } + + boolean isEndOfGroup() { + return group && groupname == null; + } + + @Override + public boolean isGroup() { + return group; + } + + @Override + public InternetAddress[] getGroup(boolean strict) throws AddressException { + if (grouplist == null) + return null; + return grouplist.clone(); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/FLAGS.java b/app/src/main/java/com/sun/mail/imap/protocol/FLAGS.java new file mode 100644 index 0000000000..5621aa81c3 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/FLAGS.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import javax.mail.Flags; +import com.sun.mail.iap.*; + +/** + * This class + * + * @author John Mani + */ + +public class FLAGS extends Flags implements Item { + + // IMAP item name + static final char[] name = {'F','L','A','G','S'}; + public int msgno; + + private static final long serialVersionUID = 439049847053756670L; + + /** + * Constructor. + * + * @param r the IMAPResponse + * @exception ParsingException for parsing failures + */ + public FLAGS(IMAPResponse r) throws ParsingException { + msgno = r.getNumber(); + + r.skipSpaces(); + String[] flags = r.readSimpleList(); + if (flags != null) { // if not empty flaglist + for (int i = 0; i < flags.length; i++) { + String s = flags[i]; + if (s.length() >= 2 && s.charAt(0) == '\\') { + switch (Character.toUpperCase(s.charAt(1))) { + case 'S': // \Seen + add(Flags.Flag.SEEN); + break; + case 'R': // \Recent + add(Flags.Flag.RECENT); + break; + case 'D': + if (s.length() >= 3) { + char c = s.charAt(2); + if (c == 'e' || c == 'E') // \Deleted + add(Flags.Flag.DELETED); + else if (c == 'r' || c == 'R') // \Draft + add(Flags.Flag.DRAFT); + } else + add(s); // unknown, treat it as a user flag + break; + case 'A': // \Answered + add(Flags.Flag.ANSWERED); + break; + case 'F': // \Flagged + add(Flags.Flag.FLAGGED); + break; + case '*': // \* + add(Flags.Flag.USER); + break; + default: + add(s); // unknown, treat it as a user flag + break; + } + } else + add(s); + } + } + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/FetchItem.java b/app/src/main/java/com/sun/mail/imap/protocol/FetchItem.java new file mode 100644 index 0000000000..1eaae6c1c7 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/FetchItem.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.lang.reflect.*; + +import javax.mail.FetchProfile; +import com.sun.mail.iap.ParsingException; + +/** + * Metadata describing a FETCH item. + * Note that the "name" field MUST be in uppercase.

+ * + * @author Bill Shannon + * @since JavaMail 1.4.6 + */ + +public abstract class FetchItem { + private String name; + private FetchProfile.Item fetchProfileItem; + + public FetchItem(String name, FetchProfile.Item fetchProfileItem) { + this.name = name; + this.fetchProfileItem = fetchProfileItem; + } + + public String getName() { + return name; + } + + public FetchProfile.Item getFetchProfileItem() { + return fetchProfileItem; + } + + /** + * Parse the item into some kind of object appropriate for the item. + * Note that the item name will have been parsed and skipped already. + * + * @param r the response + * @return the fetch item + * @exception ParsingException for parsing failures + */ + public abstract Object parseItem(FetchResponse r) throws ParsingException; +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/FetchResponse.java b/app/src/main/java/com/sun/mail/imap/protocol/FetchResponse.java new file mode 100644 index 0000000000..6477ba5b2d --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/FetchResponse.java @@ -0,0 +1,320 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.io.*; +import java.util.*; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.iap.*; + +/** + * This class represents a FETCH response obtained from the input stream + * of an IMAP server. + * + * @author John Mani + * @author Bill Shannon + */ + +public class FetchResponse extends IMAPResponse { + /* + * Regular Items are saved in the items array. + * Extension items (items handled by subclasses + * that extend the IMAP provider) are saved in the + * extensionItems map, indexed by the FETCH item name. + * The map is only created when needed. + * + * XXX - Should consider unifying the handling of + * regular items and extension items. + */ + private Item[] items; + private Map extensionItems; + private final FetchItem[] fitems; + + public FetchResponse(Protocol p) + throws IOException, ProtocolException { + super(p); + fitems = null; + parse(); + } + + public FetchResponse(IMAPResponse r) + throws IOException, ProtocolException { + this(r, null); + } + + /** + * Construct a FetchResponse that handles the additional FetchItems. + * + * @param r the IMAPResponse + * @param fitems the fetch items + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + * @since JavaMail 1.4.6 + */ + public FetchResponse(IMAPResponse r, FetchItem[] fitems) + throws IOException, ProtocolException { + super(r); + this.fitems = fitems; + parse(); + } + + public int getItemCount() { + return items.length; + } + + public Item getItem(int index) { + return items[index]; + } + + public T getItem(Class c) { + for (int i = 0; i < items.length; i++) { + if (c.isInstance(items[i])) + return c.cast(items[i]); + } + + return null; + } + + /** + * Return the first fetch response item of the given class + * for the given message number. + * + * @param r the responses + * @param msgno the message number + * @param c the class + * @param the type of fetch item + * @return the fetch item + */ + public static T getItem(Response[] r, int msgno, + Class c) { + if (r == null) + return null; + + for (int i = 0; i < r.length; i++) { + + if (r[i] == null || + !(r[i] instanceof FetchResponse) || + ((FetchResponse)r[i]).getNumber() != msgno) + continue; + + FetchResponse f = (FetchResponse)r[i]; + for (int j = 0; j < f.items.length; j++) { + if (c.isInstance(f.items[j])) + return c.cast(f.items[j]); + } + } + + return null; + } + + /** + * Return all fetch response items of the given class + * for the given message number. + * + * @param r the responses + * @param msgno the message number + * @param c the class + * @param the type of fetch items + * @return the list of fetch items + * @since JavaMail 1.5.2 + */ + public static List getItems(Response[] r, int msgno, + Class c) { + List items = new ArrayList<>(); + + if (r == null) + return items; + + for (int i = 0; i < r.length; i++) { + + if (r[i] == null || + !(r[i] instanceof FetchResponse) || + ((FetchResponse)r[i]).getNumber() != msgno) + continue; + + FetchResponse f = (FetchResponse)r[i]; + for (int j = 0; j < f.items.length; j++) { + if (c.isInstance(f.items[j])) + items.add(c.cast(f.items[j])); + } + } + + return items; + } + + /** + * Return a map of the extension items found in this fetch response. + * The map is indexed by extension item name. Callers should not + * modify the map. + * + * @return Map of extension items, or null if none + * @since JavaMail 1.4.6 + */ + public Map getExtensionItems() { + return extensionItems; + } + + private final static char[] HEADER = {'.','H','E','A','D','E','R'}; + private final static char[] TEXT = {'.','T','E','X','T'}; + + private void parse() throws ParsingException { + if (!isNextNonSpace('(')) + throw new ParsingException( + "error in FETCH parsing, missing '(' at index " + index); + + List v = new ArrayList<>(); + Item i = null; + skipSpaces(); + do { + + if (index >= size) + throw new ParsingException( + "error in FETCH parsing, ran off end of buffer, size " + size); + + i = parseItem(); + if (i != null) + v.add(i); + else if (!parseExtensionItem()) + throw new ParsingException( + "error in FETCH parsing, unrecognized item at index " + + index + ", starts with \"" + next20() + "\""); + } while (!isNextNonSpace(')')); + + items = v.toArray(new Item[v.size()]); + } + + /** + * Return the next 20 characters in the buffer, for exception messages. + */ + private String next20() { + if (index + 20 > size) + return ASCIIUtility.toString(buffer, index, size); + else + return ASCIIUtility.toString(buffer, index, index + 20) + "..."; + } + + /** + * Parse the item at the current position in the buffer, + * skipping over the item if successful. Otherwise, return null + * and leave the buffer position unmodified. + */ + @SuppressWarnings("empty") + private Item parseItem() throws ParsingException { + switch (buffer[index]) { + case 'E': case 'e': + if (match(ENVELOPE.name)) + return new ENVELOPE(this); + break; + case 'F': case 'f': + if (match(FLAGS.name)) + return new FLAGS((IMAPResponse)this); + break; + case 'I': case 'i': + if (match(INTERNALDATE.name)) + return new INTERNALDATE(this); + break; + case 'B': case 'b': + if (match(BODYSTRUCTURE.name)) + return new BODYSTRUCTURE(this); + else if (match(BODY.name)) { + if (buffer[index] == '[') + return new BODY(this); + else + return new BODYSTRUCTURE(this); + } + break; + case 'R': case 'r': + if (match(RFC822SIZE.name)) + return new RFC822SIZE(this); + else if (match(RFC822DATA.name)) { + boolean isHeader = false; + if (match(HEADER)) + isHeader = true; // skip ".HEADER" + else if (match(TEXT)) + isHeader = false; // skip ".TEXT" + return new RFC822DATA(this, isHeader); + } + break; + case 'U': case 'u': + if (match(UID.name)) + return new UID(this); + break; + case 'M': case 'm': + if (match(MODSEQ.name)) + return new MODSEQ(this); + break; + default: + break; + } + return null; + } + + /** + * If this item is a known extension item, parse it. + */ + private boolean parseExtensionItem() throws ParsingException { + if (fitems == null) + return false; + for (int i = 0; i < fitems.length; i++) { + if (match(fitems[i].getName())) { + if (extensionItems == null) + extensionItems = new HashMap<>(); + extensionItems.put(fitems[i].getName(), + fitems[i].parseItem(this)); + return true; + } + } + return false; + } + + /** + * Does the current buffer match the given item name? + * itemName is the name of the IMAP item to compare against. + * NOTE that itemName *must* be all uppercase. + * If the match is successful, the buffer pointer (index) + * is incremented past the matched item. + */ + private boolean match(char[] itemName) { + int len = itemName.length; + for (int i = 0, j = index; i < len;) + // IMAP tokens are case-insensitive. We store itemNames in + // uppercase, so convert operand to uppercase before comparing. + if (Character.toUpperCase((char)buffer[j++]) != itemName[i++]) + return false; + index += len; + return true; + } + + /** + * Does the current buffer match the given item name? + * itemName is the name of the IMAP item to compare against. + * NOTE that itemName *must* be all uppercase. + * If the match is successful, the buffer pointer (index) + * is incremented past the matched item. + */ + private boolean match(String itemName) { + int len = itemName.length(); + for (int i = 0, j = index; i < len;) + // IMAP tokens are case-insensitive. We store itemNames in + // uppercase, so convert operand to uppercase before comparing. + if (Character.toUpperCase((char)buffer[j++]) != + itemName.charAt(i++)) + return false; + index += len; + return true; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/ID.java b/app/src/main/java/com/sun/mail/imap/protocol/ID.java new file mode 100644 index 0000000000..66cb865fcc --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/ID.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.*; +import com.sun.mail.iap.*; + +/** + * This class represents the response to the ID command.

+ * + * See RFC 2971. + * + * @since JavaMail 1.5.1 + * @author Bill Shannon + */ + +public class ID { + + private Map serverParams = null; + + /** + * Parse the server parameter list out of the response. + * + * @param r the response + * @exception ProtocolException for protocol failures + */ + public ID(Response r) throws ProtocolException { + // id_response ::= "ID" SPACE id_params_list + // id_params_list ::= "(" #(string SPACE nstring) ")" / nil + // ;; list of field value pairs + + r.skipSpaces(); + int c = r.peekByte(); + if (c == 'N' || c == 'n') // assume NIL + return; + + if (c != '(') + throw new ProtocolException("Missing '(' at start of ID"); + + serverParams = new HashMap<>(); + + String[] v = r.readStringList(); + if (v != null) { + for (int i = 0; i < v.length; i += 2) { + String name = v[i]; + if (name == null) + throw new ProtocolException("ID field name null"); + if (i + 1 >= v.length) + throw new ProtocolException("ID field without value: " + + name); + String value = v[i + 1]; + serverParams.put(name, value); + } + } + serverParams = Collections.unmodifiableMap(serverParams); + } + + /** + * Return the parsed server params. + */ + Map getServerParams() { + return serverParams; + } + + /** + * Convert the client parameters into an argument list for the ID command. + */ + static Argument getArgumentList(Map clientParams) { + Argument arg = new Argument(); + if (clientParams == null) { + arg.writeAtom("NIL"); + return arg; + } + Argument list = new Argument(); + // add params to list + for (Map.Entry e : clientParams.entrySet()) { + list.writeNString(e.getKey()); // assume these are ASCII only + list.writeNString(e.getValue()); + } + arg.writeArgument(list); + return arg; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/IMAPProtocol.java b/app/src/main/java/com/sun/mail/imap/protocol/IMAPProtocol.java new file mode 100644 index 0000000000..a8a88b6c01 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/IMAPProtocol.java @@ -0,0 +1,3294 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.io.*; +import java.util.*; +import java.text.*; +import java.lang.reflect.*; +import java.util.logging.Level; +import java.nio.charset.StandardCharsets; + +import javax.mail.*; +import javax.mail.internet.*; +import javax.mail.search.*; + +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.util.BASE64EncoderStream; +import com.sun.mail.iap.*; +import com.sun.mail.auth.Ntlm; + +import com.sun.mail.imap.ACL; +import com.sun.mail.imap.Rights; +import com.sun.mail.imap.AppendUID; +import com.sun.mail.imap.CopyUID; +import com.sun.mail.imap.SortTerm; +import com.sun.mail.imap.ResyncData; +import com.sun.mail.imap.Utility; + +/** + * This class extends the iap.Protocol object and implements IMAP + * semantics. In general, there is a method corresponding to each + * IMAP protocol command. The typical implementation issues the + * appropriate protocol command, collects all responses, processes + * those responses that are specific to this command and then + * dispatches the rest (the unsolicited ones) to the dispatcher + * using the notifyResponseHandlers(r). + * + * @author John Mani + * @author Bill Shannon + */ + +public class IMAPProtocol extends Protocol { + + private boolean connected = false; // did constructor succeed? + private boolean rev1 = false; // REV1 server ? + private boolean referralException; // throw exception for IMAP REFERRAL? + private boolean noauthdebug = true; // hide auth info in debug output + private boolean authenticated; // authenticated? + // WARNING: authenticated may be set to true in superclass + // constructor, don't initialize it here. + + private Map capabilities; + // WARNING: capabilities may be initialized as a result of superclass + // constructor, don't initialize it here. + private List authmechs; + // WARNING: authmechs may be initialized as a result of superclass + // constructor, don't initialize it here. + private boolean utf8; // UTF-8 support enabled? + + protected SearchSequence searchSequence; + protected String[] searchCharsets; // array of search charsets + + protected Set enabled; // enabled capabilities - RFC 5161 + + private String name; + private SaslAuthenticator saslAuthenticator; // if SASL is being used + private String proxyAuthUser; // user name used with PROXYAUTH + + private ByteArray ba; // a buffer for fetchBody + + private static final byte[] CRLF = { (byte)'\r', (byte)'\n'}; + + private static final FetchItem[] fetchItems = { }; + + /** + * Constructor. + * Opens a connection to the given host at given port. + * + * @param name the protocol name + * @param host host to connect to + * @param port port number to connect to + * @param props Properties object used by this protocol + * @param isSSL true if SSL should be used + * @param logger the MailLogger to use for debug output + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + public IMAPProtocol(String name, String host, int port, + Properties props, boolean isSSL, MailLogger logger) + throws IOException, ProtocolException { + super(host, port, props, "mail." + name, isSSL, logger); + + try { + this.name = name; + noauthdebug = + !PropUtil.getBooleanProperty(props, "mail.debug.auth", false); + + // in case it was not initialized in processGreeting + referralException = PropUtil.getBooleanProperty(props, + prefix + ".referralexception", false); + + if (capabilities == null) + capability(); + + if (hasCapability("IMAP4rev1")) + rev1 = true; + + searchCharsets = new String[2]; // 2, for now. + searchCharsets[0] = "UTF-8"; + searchCharsets[1] = MimeUtility.mimeCharset( + MimeUtility.getDefaultJavaCharset() + ); + + connected = true; // must be last statement in constructor + } finally { + /* + * If we get here because an exception was thrown, we need + * to disconnect to avoid leaving a connected socket that + * no one will be able to use because this object was never + * completely constructed. + */ + if (!connected) + disconnect(); + } + } + + /** + * Constructor for debugging. + * + * @param in the InputStream from which to read + * @param out the PrintStream to which to write + * @param props Properties object used by this protocol + * @param debug true to enable debugging output + * @exception IOException for I/O errors + */ + public IMAPProtocol(InputStream in, PrintStream out, + Properties props, boolean debug) + throws IOException { + super(in, out, props, debug); + + this.name = "imap"; + noauthdebug = + !PropUtil.getBooleanProperty(props, "mail.debug.auth", false); + + if (capabilities == null) + capabilities = new HashMap<>(); + + searchCharsets = new String[2]; // 2, for now. + searchCharsets[0] = "UTF-8"; + searchCharsets[1] = MimeUtility.mimeCharset( + MimeUtility.getDefaultJavaCharset() + ); + + connected = true; // must be last statement in constructor + } + + /** + * Return an array of FetchItem objects describing the + * FETCH items supported by this protocol. Subclasses may + * override this method to combine their FetchItems with + * the FetchItems returned by the superclass. + * + * @return an array of FetchItem objects + * @since JavaMail 1.4.6 + */ + public FetchItem[] getFetchItems() { + return fetchItems; + } + + /** + * CAPABILITY command. + * + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.1.1" + */ + public void capability() throws ProtocolException { + // Check CAPABILITY + Response[] r = command("CAPABILITY", null); + Response response = r[r.length-1]; + + if (response.isOK()) + handleCapabilityResponse(r); + handleResult(response); + } + + /** + * Handle any untagged CAPABILITY response in the Response array. + * + * @param r the responses + */ + public void handleCapabilityResponse(Response[] r) { + boolean first = true; + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + + // Handle *all* untagged CAPABILITY responses. + // Though the spec seemingly states that only + // one CAPABILITY response string is allowed (6.1.1), + // some server vendors claim otherwise. + if (ir.keyEquals("CAPABILITY")) { + if (first) { + // clear out current when first response seen + capabilities = new HashMap<>(10); + authmechs = new ArrayList<>(5); + first = false; + } + parseCapabilities(ir); + } + } + } + + /** + * If the response contains a CAPABILITY response code, extract + * it and save the capabilities. + * + * @param r the response + */ + protected void setCapabilities(Response r) { + byte b; + while ((b = r.readByte()) > 0 && b != (byte)'[') + ; + if (b == 0) + return; + String s; + s = r.readAtom(); + if (!s.equalsIgnoreCase("CAPABILITY")) + return; + capabilities = new HashMap<>(10); + authmechs = new ArrayList<>(5); + parseCapabilities(r); + } + + /** + * Parse the capabilities from a CAPABILITY response or from + * a CAPABILITY response code attached to (e.g.) an OK response. + * + * @param r the CAPABILITY response + */ + protected void parseCapabilities(Response r) { + String s; + while ((s = r.readAtom()) != null) { + if (s.length() == 0) { + if (r.peekByte() == (byte)']') + break; + /* + * Probably found something here that's not an atom. + * Rather than loop forever or fail completely, we'll + * try to skip this bogus capability. This is known + * to happen with: + * Netscape Messaging Server 4.03 (built Apr 27 1999) + * that returns: + * * CAPABILITY * CAPABILITY IMAP4 IMAP4rev1 ... + * The "*" in the middle of the capability list causes + * us to loop forever here. + */ + r.skipToken(); + } else { + capabilities.put(s.toUpperCase(Locale.ENGLISH), s); + if (s.regionMatches(true, 0, "AUTH=", 0, 5)) { + authmechs.add(s.substring(5)); + if (logger.isLoggable(Level.FINE)) + logger.fine("AUTH: " + s.substring(5)); + } + } + } + } + + /** + * Check the greeting when first connecting; look for PREAUTH response. + * + * @param r the greeting response + * @exception ProtocolException for protocol failures + */ + @Override + protected void processGreeting(Response r) throws ProtocolException { + if (r.isBYE()) { + checkReferral(r); // may throw exception + throw new ConnectionException(this, r); + } + if (r.isOK()) { // check if it's OK + // XXX - is a REFERRAL response code really allowed here? + // XXX - referralException hasn't been initialized in c'tor yet + referralException = PropUtil.getBooleanProperty(props, + prefix + ".referralexception", false); + if (referralException) + checkReferral(r); + setCapabilities(r); + return; + } + // only other choice is PREAUTH + assert r instanceof IMAPResponse; + IMAPResponse ir = (IMAPResponse)r; + if (ir.keyEquals("PREAUTH")) { + authenticated = true; + setCapabilities(r); + } else { + disconnect(); + throw new ConnectionException(this, r); + } + } + + /** + * Check for an IMAP login REFERRAL response code. + * + * @exception IMAPReferralException if REFERRAL response code found + * @see "RFC 2221" + */ + private void checkReferral(Response r) throws IMAPReferralException { + String s = r.getRest(); // get the text after the response + if (s.startsWith("[")) { // a response code + int i = s.indexOf(' '); + if (i > 0 && s.substring(1, i).equalsIgnoreCase("REFERRAL")) { + String url, msg; + int j = s.indexOf(']'); + if (j > 0) { // should always be true; + url = s.substring(i + 1, j); + msg = s.substring(j + 1).trim(); + } else { + url = s.substring(i + 1); + msg = ""; + } + if (r.isBYE()) + disconnect(); + throw new IMAPReferralException(msg, url); + } + } + } + + /** + * Returns true if the connection has been authenticated, + * either due to a successful login, or due to a PREAUTH greeting response. + * + * @return true if the connection has been authenticated + */ + public boolean isAuthenticated() { + return authenticated; + } + + /** + * Returns true if this is an IMAP4rev1 server + * + * @return true if this is an IMAP4rev1 server + */ + public boolean isREV1() { + return rev1; + } + + /** + * Returns whether this Protocol supports non-synchronizing literals. + * + * @return true if non-synchronizing literals are supported + */ + @Override + protected boolean supportsNonSyncLiterals() { + return hasCapability("LITERAL+"); + } + + /** + * Read a response from the server. + * + * @return the response + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + @Override + public Response readResponse() throws IOException, ProtocolException { + // assert Thread.holdsLock(this); + // can't assert because it's called from constructor + IMAPResponse r = new IMAPResponse(this); + if (r.keyEquals("FETCH")) + r = new FetchResponse(r, getFetchItems()); + return r; + } + + /** + * Check whether the given capability is supported by + * this server. Returns true if so, otherwise + * returns false. + * + * @param c the capability name + * @return true if the server has the capability + */ + public boolean hasCapability(String c) { + if (c.endsWith("*")) { + c = c.substring(0, c.length() - 1).toUpperCase(Locale.ENGLISH); + Iterator it = capabilities.keySet().iterator(); + while (it.hasNext()) { + if (it.next().startsWith(c)) + return true; + } + return false; + } + return capabilities.containsKey(c.toUpperCase(Locale.ENGLISH)); + } + + /** + * Return the map of capabilities returned by the server. + * + * @return the Map of capabilities + * @since JavaMail 1.4.1 + */ + public Map getCapabilities() { + return capabilities; + } + + /** + * Does the server support UTF-8? + * + * @since JavaMail 1.6.0 + */ + public boolean supportsUtf8() { + return utf8; + } + + /** + * Close socket connection. + * + * This method just makes the Protocol.disconnect() method + * public. + */ + @Override + public void disconnect() { + super.disconnect(); + authenticated = false; // just in case + } + + /** + * The NOOP command. + * + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.1.2" + */ + public void noop() throws ProtocolException { + logger.fine("IMAPProtocol noop"); + simpleCommand("NOOP", null); + } + + /** + * LOGOUT Command. + * + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.1.3" + */ + public void logout() throws ProtocolException { + try { + Response[] r = command("LOGOUT", null); + + authenticated = false; + // dispatch any unsolicited responses. + // NOTE that the BYE response is dispatched here as well + notifyResponseHandlers(r); + } finally { + disconnect(); + } + } + + /** + * LOGIN Command. + * + * @param u the username + * @param p the password + * @throws ProtocolException as thrown by {@link Protocol#handleResult}. + * @see "RFC2060, section 6.2.2" + */ + public void login(String u, String p) throws ProtocolException { + Argument args = new Argument(); + args.writeString(u); + args.writeString(p); + + Response[] r = null; + try { + if (noauthdebug && isTracing()) { + logger.fine("LOGIN command trace suppressed"); + suspendTracing(); + } + r = command("LOGIN", args); + } finally { + resumeTracing(); + } + + // handle an illegal but not uncommon untagged CAPABILTY response + handleCapabilityResponse(r); + + // dispatch untagged responses + notifyResponseHandlers(r); + + // Handle result of this command + if (noauthdebug && isTracing()) + logger.fine("LOGIN command result: " + r[r.length-1]); + handleLoginResult(r[r.length-1]); + // If the response includes a CAPABILITY response code, process it + setCapabilities(r[r.length-1]); + // if we get this far without an exception, we're authenticated + authenticated = true; + } + + /** + * The AUTHENTICATE command with AUTH=LOGIN authenticate scheme + * + * @param u the username + * @param p the password + * @throws ProtocolException as thrown by {@link Protocol#handleResult}. + * @see "RFC2060, section 6.2.1" + */ + public synchronized void authlogin(String u, String p) + throws ProtocolException { + List v = new ArrayList<>(); + String tag = null; + Response r = null; + boolean done = false; + + try { + + if (noauthdebug && isTracing()) { + logger.fine("AUTHENTICATE LOGIN command trace suppressed"); + suspendTracing(); + } + + try { + tag = writeCommand("AUTHENTICATE LOGIN", null); + } catch (Exception ex) { + // Convert this into a BYE response + r = Response.byeResponse(ex); + done = true; + } + + OutputStream os = getOutputStream(); // stream to IMAP server + + /* Wrap a BASE64Encoder around a ByteArrayOutputstream + * to craft b64 encoded username and password strings + * + * Note that the encoded bytes should be sent "as-is" to the + * server, *not* as literals or quoted-strings. + * + * Also note that unlike the B64 definition in MIME, CRLFs + * should *not* be inserted during the encoding process. So, I + * use Integer.MAX_VALUE (0x7fffffff (> 1G)) as the bytesPerLine, + * which should be sufficiently large ! + * + * Finally, format the line in a buffer so it can be sent as + * a single packet, to avoid triggering a bug in SUN's SIMS 2.0 + * server caused by patch 105346. + */ + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + OutputStream b64os = new BASE64EncoderStream(bos, Integer.MAX_VALUE); + boolean first = true; + + while (!done) { // loop till we are done + try { + r = readResponse(); + if (r.isContinuation()) { + // Server challenge .. + String s; + if (first) { // Send encoded username + s = u; + first = false; + } else // Send encoded password + s = p; + + // obtain b64 encoded bytes + b64os.write(s.getBytes(StandardCharsets.UTF_8)); + b64os.flush(); // complete the encoding + + bos.write(CRLF); // CRLF termination + os.write(bos.toByteArray()); // write out line + os.flush(); // flush the stream + bos.reset(); // reset buffer + } else if (r.isTagged() && r.getTag().equals(tag)) + // Ah, our tagged response + done = true; + else if (r.isBYE()) // outta here + done = true; + // hmm .. unsolicited response here ?! + } catch (Exception ioex) { + // convert this into a BYE response + r = Response.byeResponse(ioex); + done = true; + } + v.add(r); + } + + } finally { + resumeTracing(); + } + + Response[] responses = v.toArray(new Response[v.size()]); + + // handle an illegal but not uncommon untagged CAPABILTY response + handleCapabilityResponse(responses); + + /* + * Dispatch untagged responses. + * NOTE: in our current upper level IMAP classes, we add the + * responseHandler to the Protocol object only *after* the + * connection has been authenticated. So, for now, the below + * code really ends up being just a no-op. + */ + notifyResponseHandlers(responses); + + // Handle the final OK, NO, BAD or BYE response + if (noauthdebug && isTracing()) + logger.fine("AUTHENTICATE LOGIN command result: " + r); + handleLoginResult(r); + // If the response includes a CAPABILITY response code, process it + setCapabilities(r); + // if we get this far without an exception, we're authenticated + authenticated = true; + } + + + /** + * The AUTHENTICATE command with AUTH=PLAIN authentication scheme. + * This is based heavly on the {@link #authlogin} method. + * + * @param authzid the authorization id + * @param u the username + * @param p the password + * @throws ProtocolException as thrown by {@link Protocol#handleResult}. + * @see "RFC3501, section 6.2.2" + * @see "RFC2595, section 6" + * @since JavaMail 1.3.2 + */ + public synchronized void authplain(String authzid, String u, String p) + throws ProtocolException { + List v = new ArrayList<>(); + String tag = null; + Response r = null; + boolean done = false; + + try { + + if (noauthdebug && isTracing()) { + logger.fine("AUTHENTICATE PLAIN command trace suppressed"); + suspendTracing(); + } + + try { + tag = writeCommand("AUTHENTICATE PLAIN", null); + } catch (Exception ex) { + // Convert this into a BYE response + r = Response.byeResponse(ex); + done = true; + } + + OutputStream os = getOutputStream(); // stream to IMAP server + + /* Wrap a BASE64Encoder around a ByteArrayOutputstream + * to craft b64 encoded username and password strings + * + * Note that the encoded bytes should be sent "as-is" to the + * server, *not* as literals or quoted-strings. + * + * Also note that unlike the B64 definition in MIME, CRLFs + * should *not* be inserted during the encoding process. So, I + * use Integer.MAX_VALUE (0x7fffffff (> 1G)) as the bytesPerLine, + * which should be sufficiently large ! + * + * Finally, format the line in a buffer so it can be sent as + * a single packet, to avoid triggering a bug in SUN's SIMS 2.0 + * server caused by patch 105346. + */ + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + OutputStream b64os = new BASE64EncoderStream(bos, Integer.MAX_VALUE); + + while (!done) { // loop till we are done + try { + r = readResponse(); + if (r.isContinuation()) { + // Server challenge .. + final String nullByte = "\0"; + String s = (authzid == null ? "" : authzid) + + nullByte + u + nullByte + p; + + // obtain b64 encoded bytes + b64os.write(s.getBytes(StandardCharsets.UTF_8)); + b64os.flush(); // complete the encoding + + bos.write(CRLF); // CRLF termination + os.write(bos.toByteArray()); // write out line + os.flush(); // flush the stream + bos.reset(); // reset buffer + } else if (r.isTagged() && r.getTag().equals(tag)) + // Ah, our tagged response + done = true; + else if (r.isBYE()) // outta here + done = true; + // hmm .. unsolicited response here ?! + } catch (Exception ioex) { + // convert this into a BYE response + r = Response.byeResponse(ioex); + done = true; + } + v.add(r); + } + + } finally { + resumeTracing(); + } + + Response[] responses = v.toArray(new Response[v.size()]); + + // handle an illegal but not uncommon untagged CAPABILTY response + handleCapabilityResponse(responses); + + /* + * Dispatch untagged responses. + * NOTE: in our current upper level IMAP classes, we add the + * responseHandler to the Protocol object only *after* the + * connection has been authenticated. So, for now, the below + * code really ends up being just a no-op. + */ + notifyResponseHandlers(responses); + + // Handle the final OK, NO, BAD or BYE response + if (noauthdebug && isTracing()) + logger.fine("AUTHENTICATE PLAIN command result: " + r); + handleLoginResult(r); + // If the response includes a CAPABILITY response code, process it + setCapabilities(r); + // if we get this far without an exception, we're authenticated + authenticated = true; + } + + /** + * The AUTHENTICATE command with AUTH=NTLM authentication scheme. + * This is based heavly on the {@link #authlogin} method. + * + * @param authzid the authorization id + * @param u the username + * @param p the password + * @throws ProtocolException as thrown by {@link Protocol#handleResult}. + * @see "RFC3501, section 6.2.2" + * @see "RFC2595, section 6" + * @since JavaMail 1.4.3 + */ + public synchronized void authntlm(String authzid, String u, String p) + throws ProtocolException { + List v = new ArrayList<>(); + String tag = null; + Response r = null; + boolean done = false; + + String type1Msg = null; + int flags = PropUtil.getIntProperty(props, + "mail." + name + ".auth.ntlm.flags", 0); + boolean v2 = PropUtil.getBooleanProperty(props, + "mail." + name + ".auth.ntlm.v2", true); + String domain = props.getProperty( + "mail." + name + ".auth.ntlm.domain", ""); + Ntlm ntlm = new Ntlm(domain, getLocalHost(), u, p, logger); + + try { + + if (noauthdebug && isTracing()) { + logger.fine("AUTHENTICATE NTLM command trace suppressed"); + suspendTracing(); + } + + try { + tag = writeCommand("AUTHENTICATE NTLM", null); + } catch (Exception ex) { + // Convert this into a BYE response + r = Response.byeResponse(ex); + done = true; + } + + OutputStream os = getOutputStream(); // stream to IMAP server + boolean first = true; + + while (!done) { // loop till we are done + try { + r = readResponse(); + if (r.isContinuation()) { + // Server challenge .. + String s; + if (first) { + s = ntlm.generateType1Msg(flags, v2); + first = false; + } else { + s = ntlm.generateType3Msg(r.getRest()); + } + + os.write(s.getBytes(StandardCharsets.UTF_8)); + os.write(CRLF); // CRLF termination + os.flush(); // flush the stream + } else if (r.isTagged() && r.getTag().equals(tag)) + // Ah, our tagged response + done = true; + else if (r.isBYE()) // outta here + done = true; + // hmm .. unsolicited response here ?! + } catch (Exception ioex) { + // convert this into a BYE response + r = Response.byeResponse(ioex); + done = true; + } + v.add(r); + } + + } finally { + resumeTracing(); + } + + Response[] responses = v.toArray(new Response[v.size()]); + + // handle an illegal but not uncommon untagged CAPABILTY response + handleCapabilityResponse(responses); + + /* + * Dispatch untagged responses. + * NOTE: in our current upper level IMAP classes, we add the + * responseHandler to the Protocol object only *after* the + * connection has been authenticated. So, for now, the below + * code really ends up being just a no-op. + */ + notifyResponseHandlers(responses); + + // Handle the final OK, NO, BAD or BYE response + if (noauthdebug && isTracing()) + logger.fine("AUTHENTICATE NTLM command result: " + r); + handleLoginResult(r); + // If the response includes a CAPABILITY response code, process it + setCapabilities(r); + // if we get this far without an exception, we're authenticated + authenticated = true; + } + + /** + * The AUTHENTICATE command with AUTH=XOAUTH2 authentication scheme. + * This is based heavly on the {@link #authlogin} method. + * + * @param u the username + * @param p the password + * @throws ProtocolException as thrown by {@link Protocol#handleResult}. + * @see "RFC3501, section 6.2.2" + * @see "RFC2595, section 6" + * @since JavaMail 1.5.5 + */ + public synchronized void authoauth2(String u, String p) + throws ProtocolException { + List v = new ArrayList<>(); + String tag = null; + Response r = null; + boolean done = false; + + try { + + if (noauthdebug && isTracing()) { + logger.fine("AUTHENTICATE XOAUTH2 command trace suppressed"); + suspendTracing(); + } + + try { + Argument args = new Argument(); + args.writeAtom("XOAUTH2"); + if (hasCapability("SASL-IR")) { + String resp = "user=" + u + "\001auth=Bearer " + p + "\001\001"; + byte[] ba = BASE64EncoderStream.encode( + resp.getBytes(StandardCharsets.UTF_8)); + String irs = ASCIIUtility.toString(ba, 0, ba.length); + args.writeAtom(irs); + } + tag = writeCommand("AUTHENTICATE", args); + } catch (Exception ex) { + // Convert this into a BYE response + r = Response.byeResponse(ex); + done = true; + } + + OutputStream os = getOutputStream(); // stream to IMAP server + + while (!done) { // loop till we are done + try { + r = readResponse(); + if (r.isContinuation()) { + // Server challenge .. + String resp = "user=" + u + "\001auth=Bearer " + + p + "\001\001"; + byte[] b = BASE64EncoderStream.encode( + resp.getBytes(StandardCharsets.UTF_8)); + os.write(b); // write out response + os.write(CRLF); // CRLF termination + os.flush(); // flush the stream + } else if (r.isTagged() && r.getTag().equals(tag)) + // Ah, our tagged response + done = true; + else if (r.isBYE()) // outta here + done = true; + // hmm .. unsolicited response here ?! + } catch (Exception ioex) { + // convert this into a BYE response + r = Response.byeResponse(ioex); + done = true; + } + v.add(r); + } + + } finally { + resumeTracing(); + } + + Response[] responses = v.toArray(new Response[v.size()]); + + // handle an illegal but not uncommon untagged CAPABILTY response + handleCapabilityResponse(responses); + + /* + * Dispatch untagged responses. + * NOTE: in our current upper level IMAP classes, we add the + * responseHandler to the Protocol object only *after* the + * connection has been authenticated. So, for now, the below + * code really ends up being just a no-op. + */ + notifyResponseHandlers(responses); + + // Handle the final OK, NO, BAD or BYE response + if (noauthdebug && isTracing()) + logger.fine("AUTHENTICATE XOAUTH2 command result: " + r); + handleLoginResult(r); + // If the response includes a CAPABILITY response code, process it + setCapabilities(r); + // if we get this far without an exception, we're authenticated + authenticated = true; + } + + /** + * SASL-based login. + * + * @param allowed the SASL mechanisms we're allowed to use + * @param realm the SASL realm + * @param authzid the authorization id + * @param u the username + * @param p the password + * @exception ProtocolException for protocol failures + */ + public void sasllogin(String[] allowed, String realm, String authzid, + String u, String p) throws ProtocolException { + boolean useCanonicalHostName = PropUtil.getBooleanProperty(props, + "mail." + name + ".sasl.usecanonicalhostname", false); + String serviceHost; + if (useCanonicalHostName) + serviceHost = getInetAddress().getCanonicalHostName(); + else + serviceHost = host; + if (saslAuthenticator == null) { + try { + Class sac = Class.forName( + "com.sun.mail.imap.protocol.IMAPSaslAuthenticator"); + Constructor c = sac.getConstructor(new Class[] { + IMAPProtocol.class, + String.class, + Properties.class, + MailLogger.class, + String.class + }); + saslAuthenticator = (SaslAuthenticator)c.newInstance( + new Object[] { + this, + name, + props, + logger, + serviceHost + }); + } catch (Exception ex) { + logger.log(Level.FINE, "Can't load SASL authenticator", ex); + // probably because we're running on a system without SASL + return; // not authenticated, try without SASL + } + } + + // were any allowed mechanisms specified? + List v; + if (allowed != null && allowed.length > 0) { + // remove anything not supported by the server + v = new ArrayList<>(allowed.length); + for (int i = 0; i < allowed.length; i++) + if (authmechs.contains(allowed[i])) // XXX - case must match + v.add(allowed[i]); + } else { + // everything is allowed + v = authmechs; + } + String[] mechs = v.toArray(new String[v.size()]); + + try { + + if (noauthdebug && isTracing()) { + logger.fine("SASL authentication command trace suppressed"); + suspendTracing(); + } + + if (saslAuthenticator.authenticate(mechs, realm, authzid, u, p)) { + if (noauthdebug && isTracing()) + logger.fine("SASL authentication succeeded"); + authenticated = true; + } else { + if (noauthdebug && isTracing()) + logger.fine("SASL authentication failed"); + } + } finally { + resumeTracing(); + } + } + + // XXX - for IMAPSaslAuthenticator access to protected method + OutputStream getIMAPOutputStream() { + return getOutputStream(); + } + + /** + * Handle the result response for a LOGIN or AUTHENTICATE command. + * Look for IMAP login REFERRAL. + * + * @param r the response + * @exception ProtocolException for protocol failures + * @since JavaMail 1.5.5 + */ + protected void handleLoginResult(Response r) throws ProtocolException { + if (hasCapability("LOGIN-REFERRALS") && + (!r.isOK() || referralException)) + checkReferral(r); + handleResult(r); + } + + /** + * PROXYAUTH Command. + * + * @param u the PROXYAUTH user name + * @exception ProtocolException for protocol failures + * @see "Netscape/iPlanet/SunONE Messaging Server extension" + */ + public void proxyauth(String u) throws ProtocolException { + Argument args = new Argument(); + args.writeString(u); + + simpleCommand("PROXYAUTH", args); + proxyAuthUser = u; + } + + /** + * Get the user name used with the PROXYAUTH command. + * Returns null if PROXYAUTH was not used. + * + * @return the PROXYAUTH user name + * @since JavaMail 1.5.1 + */ + public String getProxyAuthUser() { + return proxyAuthUser; + } + + /** + * UNAUTHENTICATE Command. + * + * @exception ProtocolException for protocol failures + * @see "Netscape/iPlanet/SunONE Messaging Server extension" + * @since JavaMail 1.5.1 + */ + public void unauthenticate() throws ProtocolException { + if (!hasCapability("X-UNAUTHENTICATE")) + throw new BadCommandException("UNAUTHENTICATE not supported"); + simpleCommand("UNAUTHENTICATE", null); + authenticated = false; + } + + /** + * ID Command, for Yahoo! Mail IMAP server. + * + * @param guid the GUID + * @exception ProtocolException for protocol failures + * @deprecated As of JavaMail 1.5.1, replaced by + * {@link #id(Map) id(Map<String,String>)} + * @since JavaMail 1.4.4 + */ + @Deprecated + public void id(String guid) throws ProtocolException { + // support this for now, but remove it soon + Map gmap = new HashMap<>(); + gmap.put("GUID", guid); + id(gmap); + } + + /** + * STARTTLS Command. + * + * @exception ProtocolException for protocol failures + * @see "RFC3501, section 6.2.1" + */ + public void startTLS() throws ProtocolException { + try { + super.startTLS("STARTTLS"); + } catch (ProtocolException pex) { + logger.log(Level.FINE, "STARTTLS ProtocolException", pex); + // ProtocolException just means the command wasn't recognized, + // or failed. This should never happen if we check the + // CAPABILITY first. + throw pex; + } catch (Exception ex) { + logger.log(Level.FINE, "STARTTLS Exception", ex); + // any other exception means we have to shut down the connection + // generate an artificial BYE response and disconnect + Response[] r = { Response.byeResponse(ex) }; + notifyResponseHandlers(r); + disconnect(); + throw new ProtocolException("STARTTLS failure", ex); + } + } + + /** + * COMPRESS Command. Only supports DEFLATE. + * + * @exception ProtocolException for protocol failures + * @see "RFC 4978" + */ + public void compress() throws ProtocolException { + try { + super.startCompression("COMPRESS DEFLATE"); + } catch (ProtocolException pex) { + logger.log(Level.FINE, "COMPRESS ProtocolException", pex); + // ProtocolException just means the command wasn't recognized, + // or failed. This should never happen if we check the + // CAPABILITY first. + throw pex; + } catch (Exception ex) { + logger.log(Level.FINE, "COMPRESS Exception", ex); + // any other exception means we have to shut down the connection + // generate an artificial BYE response and disconnect + Response[] r = { Response.byeResponse(ex) }; + notifyResponseHandlers(r); + disconnect(); + throw new ProtocolException("COMPRESS failure", ex); + } + } + + /** + * Encode a mailbox name appropriately depending on whether or not + * the server supports UTF-8, and add the encoded name to the + * Argument. + * + * @param args the arguments + * @param name the name to encode + * @since JavaMail 1.6.0 + */ + protected void writeMailboxName(Argument args, String name) { + if (utf8) + args.writeString(name, StandardCharsets.UTF_8); + else + // encode the mbox as per RFC2060 + args.writeString(BASE64MailboxEncoder.encode(name)); + } + + /** + * SELECT Command. + * + * @param mbox the mailbox name + * @return MailboxInfo if successful + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.1" + */ + public MailboxInfo select(String mbox) throws ProtocolException { + return select(mbox, null); + } + + /** + * SELECT Command with QRESYNC data. + * + * @param mbox the mailbox name + * @param rd the ResyncData + * @return MailboxInfo if successful + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.1" + * @see "RFC5162, section 3.1" + * @since JavaMail 1.5.1 + */ + public MailboxInfo select(String mbox, ResyncData rd) + throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, mbox); + + if (rd != null) { + if (rd == ResyncData.CONDSTORE) { + if (!hasCapability("CONDSTORE")) + throw new BadCommandException("CONDSTORE not supported"); + args.writeArgument(new Argument().writeAtom("CONDSTORE")); + } else { + if (!hasCapability("QRESYNC")) + throw new BadCommandException("QRESYNC not supported"); + args.writeArgument(resyncArgs(rd)); + } + } + + Response[] r = command("SELECT", args); + + // Note that MailboxInfo also removes those responses + // it knows about + MailboxInfo minfo = new MailboxInfo(r); + + // dispatch any remaining untagged responses + notifyResponseHandlers(r); + + Response response = r[r.length-1]; + + if (response.isOK()) { // command succesful + if (response.toString().indexOf("READ-ONLY") != -1) + minfo.mode = Folder.READ_ONLY; + else + minfo.mode = Folder.READ_WRITE; + } + + handleResult(response); + return minfo; + } + + /** + * EXAMINE Command. + * + * @param mbox the mailbox name + * @return MailboxInfo if successful + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.2" + */ + public MailboxInfo examine(String mbox) throws ProtocolException { + return examine(mbox, null); + } + + /** + * EXAMINE Command with QRESYNC data. + * + * @param mbox the mailbox name + * @param rd the ResyncData + * @return MailboxInfo if successful + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.2" + * @see "RFC5162, section 3.1" + * @since JavaMail 1.5.1 + */ + public MailboxInfo examine(String mbox, ResyncData rd) + throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, mbox); + + if (rd != null) { + if (rd == ResyncData.CONDSTORE) { + if (!hasCapability("CONDSTORE")) + throw new BadCommandException("CONDSTORE not supported"); + args.writeArgument(new Argument().writeAtom("CONDSTORE")); + } else { + if (!hasCapability("QRESYNC")) + throw new BadCommandException("QRESYNC not supported"); + args.writeArgument(resyncArgs(rd)); + } + } + + Response[] r = command("EXAMINE", args); + + // Note that MailboxInfo also removes those responses + // it knows about + MailboxInfo minfo = new MailboxInfo(r); + minfo.mode = Folder.READ_ONLY; // Obviously + + // dispatch any remaining untagged responses + notifyResponseHandlers(r); + + handleResult(r[r.length-1]); + return minfo; + } + + /** + * Generate a QRESYNC argument list based on the ResyncData. + */ + private static Argument resyncArgs(ResyncData rd) { + Argument cmd = new Argument(); + cmd.writeAtom("QRESYNC"); + Argument args = new Argument(); + args.writeNumber(rd.getUIDValidity()); + args.writeNumber(rd.getModSeq()); + UIDSet[] uids = Utility.getResyncUIDSet(rd); + if (uids != null) + args.writeString(UIDSet.toString(uids)); + cmd.writeArgument(args); + return cmd; + } + + /** + * ENABLE Command. + * + * @param cap the name of the capability to enable + * @exception ProtocolException for protocol failures + * @see "RFC 5161" + * @since JavaMail 1.5.1 + */ + public void enable(String cap) throws ProtocolException { + if (!hasCapability("ENABLE")) + throw new BadCommandException("ENABLE not supported"); + Argument args = new Argument(); + args.writeAtom(cap); + simpleCommand("ENABLE", args); + if (enabled == null) + enabled = new HashSet<>(); + enabled.add(cap.toUpperCase(Locale.ENGLISH)); + + // update the utf8 flag + utf8 = isEnabled("UTF8=ACCEPT"); + } + + /** + * Is the capability/extension enabled? + * + * @param cap the capability name + * @return true if enabled + * @see "RFC 5161" + * @since JavaMail 1.5.1 + */ + public boolean isEnabled(String cap) { + if (enabled == null) + return false; + else + return enabled.contains(cap.toUpperCase(Locale.ENGLISH)); + } + + /** + * UNSELECT Command. + * + * @exception ProtocolException for protocol failures + * @see "RFC 3691" + * @since JavaMail 1.4.4 + */ + public void unselect() throws ProtocolException { + if (!hasCapability("UNSELECT")) + throw new BadCommandException("UNSELECT not supported"); + simpleCommand("UNSELECT", null); + } + + /** + * STATUS Command. + * + * @param mbox the mailbox + * @param items the STATUS items to request + * @return STATUS results + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.10" + */ + public Status status(String mbox, String[] items) + throws ProtocolException { + if (!isREV1() && !hasCapability("IMAP4SUNVERSION")) + // STATUS is rev1 only, however the non-rev1 SIMS2.0 + // does support this. + throw new BadCommandException("STATUS not supported"); + + Argument args = new Argument(); + writeMailboxName(args, mbox); + + Argument itemArgs = new Argument(); + if (items == null) + items = Status.standardItems; + + for (int i = 0, len = items.length; i < len; i++) + itemArgs.writeAtom(items[i]); + args.writeArgument(itemArgs); + + Response[] r = command("STATUS", args); + + Status status = null; + Response response = r[r.length-1]; + + // Grab all STATUS responses + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("STATUS")) { + if (status == null) + status = new Status(ir); + else // collect 'em all + Status.add(status, new Status(ir)); + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return status; + } + + /** + * CREATE Command. + * + * @param mbox the mailbox to create + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.3" + */ + public void create(String mbox) throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, mbox); + + simpleCommand("CREATE", args); + } + + /** + * DELETE Command. + * + * @param mbox the mailbox to delete + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.4" + */ + public void delete(String mbox) throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, mbox); + + simpleCommand("DELETE", args); + } + + /** + * RENAME Command. + * + * @param o old mailbox name + * @param n new mailbox name + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.5" + */ + public void rename(String o, String n) throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, o); + writeMailboxName(args, n); + + simpleCommand("RENAME", args); + } + + /** + * SUBSCRIBE Command. + * + * @param mbox the mailbox + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.6" + */ + public void subscribe(String mbox) throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, mbox); + + simpleCommand("SUBSCRIBE", args); + } + + /** + * UNSUBSCRIBE Command. + * + * @param mbox the mailbox + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.7" + */ + public void unsubscribe(String mbox) throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, mbox); + + simpleCommand("UNSUBSCRIBE", args); + } + + /** + * LIST Command. + * + * @param ref reference string + * @param pattern pattern to list + * @return LIST results + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.8" + */ + public ListInfo[] list(String ref, String pattern) + throws ProtocolException { + return doList("LIST", ref, pattern); + } + + /** + * LSUB Command. + * + * @param ref reference string + * @param pattern pattern to list + * @return LSUB results + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.9" + */ + public ListInfo[] lsub(String ref, String pattern) + throws ProtocolException { + return doList("LSUB", ref, pattern); + } + + /** + * Execute the specified LIST-like command (e.g., "LIST" or "LSUB"), + * using the reference and pattern. + * + * @param cmd the list command + * @param ref the reference string + * @param pat the pattern + * @return array of ListInfo results + * @exception ProtocolException for protocol failures + * @since JavaMail 1.4.6 + */ + protected ListInfo[] doList(String cmd, String ref, String pat) + throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, ref); + writeMailboxName(args, pat); + + Response[] r = command(cmd, args); + + ListInfo[] linfo = null; + Response response = r[r.length-1]; + + if (response.isOK()) { // command succesful + List v = new ArrayList<>(1); + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals(cmd)) { + v.add(new ListInfo(ir)); + r[i] = null; + } + } + if (v.size() > 0) { + linfo = v.toArray(new ListInfo[v.size()]); + } + } + + // Dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return linfo; + } + + /** + * APPEND Command. + * + * @param mbox the mailbox + * @param f the message Flags + * @param d the message date + * @param data the message data + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.11" + */ + public void append(String mbox, Flags f, Date d, + Literal data) throws ProtocolException { + appenduid(mbox, f, d, data, false); // ignore return value + } + + /** + * APPEND Command, return uid from APPENDUID response code. + * + * @param mbox the mailbox + * @param f the message Flags + * @param d the message date + * @param data the message data + * @return APPENDUID data + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.11" + */ + public AppendUID appenduid(String mbox, Flags f, Date d, + Literal data) throws ProtocolException { + return appenduid(mbox, f, d, data, true); + } + + public AppendUID appenduid(String mbox, Flags f, Date d, + Literal data, boolean uid) throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, mbox); + + if (f != null) { // set Flags in appended message + // can't set the \Recent flag in APPEND + if (f.contains(Flags.Flag.RECENT)) { + f = new Flags(f); // copy, don't modify orig + f.remove(Flags.Flag.RECENT); // remove RECENT from copy + } + + /* + * HACK ALERT: We want the flag_list to be written out + * without any checking/processing of the bytes in it. If + * I use writeString(), the flag_list will end up being + * quoted since it contains "illegal" characters. So I + * am depending on implementation knowledge that writeAtom() + * does not do any checking/processing - it just writes out + * the bytes. What we really need is a writeFoo() that just + * dumps out its argument. + */ + args.writeAtom(createFlagList(f)); + } + if (d != null) // set INTERNALDATE in appended message + args.writeString(INTERNALDATE.format(d)); + + args.writeBytes(data); + + Response[] r = command("APPEND", args); + + // dispatch untagged responses + notifyResponseHandlers(r); + + // Handle result of this command + handleResult(r[r.length-1]); + + if (uid) + return getAppendUID(r[r.length-1]); + else + return null; + } + + /** + * If the response contains an APPENDUID response code, extract + * it and return an AppendUID object with the information. + */ + private AppendUID getAppendUID(Response r) { + if (!r.isOK()) + return null; + byte b; + while ((b = r.readByte()) > 0 && b != (byte)'[') + ; + if (b == 0) + return null; + String s; + s = r.readAtom(); + if (!s.equalsIgnoreCase("APPENDUID")) + return null; + + long uidvalidity = r.readLong(); + long uid = r.readLong(); + return new AppendUID(uidvalidity, uid); + } + + /** + * CHECK Command. + * + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.4.1" + */ + public void check() throws ProtocolException { + simpleCommand("CHECK", null); + } + + /** + * CLOSE Command. + * + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.4.2" + */ + public void close() throws ProtocolException { + simpleCommand("CLOSE", null); + } + + /** + * EXPUNGE Command. + * + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.4.3" + */ + public void expunge() throws ProtocolException { + simpleCommand("EXPUNGE", null); + } + + /** + * UID EXPUNGE Command. + * + * @param set UIDs to expunge + * @exception ProtocolException for protocol failures + * @see "RFC4315, section 2" + */ + public void uidexpunge(UIDSet[] set) throws ProtocolException { + if (!hasCapability("UIDPLUS")) + throw new BadCommandException("UID EXPUNGE not supported"); + simpleCommand("UID EXPUNGE " + UIDSet.toString(set), null); + } + + /** + * Fetch the BODYSTRUCTURE of the specified message. + * + * @param msgno the message number + * @return the BODYSTRUCTURE item + * @exception ProtocolException for protocol failures + */ + public BODYSTRUCTURE fetchBodyStructure(int msgno) + throws ProtocolException { + Response[] r = fetch(msgno, "BODYSTRUCTURE"); + notifyResponseHandlers(r); + + Response response = r[r.length-1]; + if (response.isOK()) + return FetchResponse.getItem(r, msgno, BODYSTRUCTURE.class); + else if (response.isNO()) + return null; + else { + handleResult(response); + return null; + } + } + + /** + * Fetch given BODY section, without marking the message + * as SEEN. + * + * @param msgno the message number + * @param section the body section + * @return the BODY item + * @exception ProtocolException for protocol failures + */ + public BODY peekBody(int msgno, String section) + throws ProtocolException { + return fetchBody(msgno, section, true); + } + + /** + * Fetch given BODY section. + * + * @param msgno the message number + * @param section the body section + * @return the BODY item + * @exception ProtocolException for protocol failures + */ + public BODY fetchBody(int msgno, String section) + throws ProtocolException { + return fetchBody(msgno, section, false); + } + + protected BODY fetchBody(int msgno, String section, boolean peek) + throws ProtocolException { + Response[] r; + + if (section == null) + section = ""; + String body = (peek ? "BODY.PEEK[" : "BODY[") + section + "]"; + return fetchSectionBody(msgno, section, body); + } + + /** + * Partial FETCH of given BODY section, without setting SEEN flag. + * + * @param msgno the message number + * @param section the body section + * @param start starting byte count + * @param size number of bytes to fetch + * @return the BODY item + * @exception ProtocolException for protocol failures + */ + public BODY peekBody(int msgno, String section, int start, int size) + throws ProtocolException { + return fetchBody(msgno, section, start, size, true, null); + } + + /** + * Partial FETCH of given BODY section. + * + * @param msgno the message number + * @param section the body section + * @param start starting byte count + * @param size number of bytes to fetch + * @return the BODY item + * @exception ProtocolException for protocol failures + */ + public BODY fetchBody(int msgno, String section, int start, int size) + throws ProtocolException { + return fetchBody(msgno, section, start, size, false, null); + } + + /** + * Partial FETCH of given BODY section, without setting SEEN flag. + * + * @param msgno the message number + * @param section the body section + * @param start starting byte count + * @param size number of bytes to fetch + * @param ba the buffer into which to read the response + * @return the BODY item + * @exception ProtocolException for protocol failures + */ + public BODY peekBody(int msgno, String section, int start, int size, + ByteArray ba) throws ProtocolException { + return fetchBody(msgno, section, start, size, true, ba); + } + + /** + * Partial FETCH of given BODY section. + * + * @param msgno the message number + * @param section the body section + * @param start starting byte count + * @param size number of bytes to fetch + * @param ba the buffer into which to read the response + * @return the BODY item + * @exception ProtocolException for protocol failures + */ + public BODY fetchBody(int msgno, String section, int start, int size, + ByteArray ba) throws ProtocolException { + return fetchBody(msgno, section, start, size, false, ba); + } + + protected BODY fetchBody(int msgno, String section, int start, int size, + boolean peek, ByteArray ba) throws ProtocolException { + this.ba = ba; // save for later use by getResponseBuffer + if (section == null) + section = ""; + String body = (peek ? "BODY.PEEK[" : "BODY[") + section + "]<" + + String.valueOf(start) + "." + + String.valueOf(size) + ">"; + return fetchSectionBody(msgno, section, body); + } + + /** + * Fetch the given body section of the given message, using the + * body string "body". + * + * @param msgno the message number + * @param section the body section + * @param body the body string + * @return the BODY item + * @exception ProtocolException for protocol failures + */ + protected BODY fetchSectionBody(int msgno, String section, String body) + throws ProtocolException { + Response[] r; + + r = fetch(msgno, body); + notifyResponseHandlers(r); + + Response response = r[r.length-1]; + if (response.isOK()) { + List bl = FetchResponse.getItems(r, msgno, BODY.class); + if (bl.size() == 1) + return bl.get(0); // the common case + if (logger.isLoggable(Level.FINEST)) + logger.finest("got " + bl.size() + + " BODY responses for section " + section); + // more then one BODY response, have to find the right one + for (BODY br : bl) { + if (logger.isLoggable(Level.FINEST)) + logger.finest("got BODY section " + br.getSection()); + if (br.getSection().equalsIgnoreCase(section)) + return br; // that's the one! + } + return null; // couldn't find it + } else if (response.isNO()) + return null; + else { + handleResult(response); + return null; + } + } + + /** + * Return a buffer to read a response into. + * The buffer is provided by fetchBody and is + * used only once. + * + * @return the buffer to use + */ + @Override + protected ByteArray getResponseBuffer() { + ByteArray ret = ba; + ba = null; + return ret; + } + + /** + * Fetch the specified RFC822 Data item. 'what' names + * the item to be fetched. 'what' can be null + * to fetch the whole message. + * + * @param msgno the message number + * @param what the item to fetch + * @return the RFC822DATA item + * @exception ProtocolException for protocol failures + */ + public RFC822DATA fetchRFC822(int msgno, String what) + throws ProtocolException { + Response[] r = fetch(msgno, + what == null ? "RFC822" : "RFC822." + what + ); + + // dispatch untagged responses + notifyResponseHandlers(r); + + Response response = r[r.length-1]; + if (response.isOK()) + return FetchResponse.getItem(r, msgno, RFC822DATA.class); + else if (response.isNO()) + return null; + else { + handleResult(response); + return null; + } + } + + /** + * Fetch the FLAGS for the given message. + * + * @param msgno the message number + * @return the Flags + * @exception ProtocolException for protocol failures + */ + public Flags fetchFlags(int msgno) throws ProtocolException { + Flags flags = null; + Response[] r = fetch(msgno, "FLAGS"); + + // Search for our FLAGS response + for (int i = 0, len = r.length; i < len; i++) { + if (r[i] == null || + !(r[i] instanceof FetchResponse) || + ((FetchResponse)r[i]).getNumber() != msgno) + continue; + + FetchResponse fr = (FetchResponse)r[i]; + if ((flags = fr.getItem(FLAGS.class)) != null) { + r[i] = null; // remove this response + break; + } + } + + // dispatch untagged responses + notifyResponseHandlers(r); + handleResult(r[r.length-1]); + return flags; + } + + /** + * Fetch the IMAP UID for the given message. + * + * @param msgno the message number + * @return the UID + * @exception ProtocolException for protocol failures + */ + public UID fetchUID(int msgno) throws ProtocolException { + Response[] r = fetch(msgno, "UID"); + + // dispatch untagged responses + notifyResponseHandlers(r); + + Response response = r[r.length-1]; + if (response.isOK()) + return FetchResponse.getItem(r, msgno, UID.class); + else if (response.isNO()) // XXX: Issue NOOP ? + return null; + else { + handleResult(response); + return null; // NOTREACHED + } + } + + /** + * Fetch the IMAP MODSEQ for the given message. + * + * @param msgno the message number + * @return the MODSEQ + * @exception ProtocolException for protocol failures + * @since JavaMail 1.5.1 + */ + public MODSEQ fetchMODSEQ(int msgno) throws ProtocolException { + Response[] r = fetch(msgno, "MODSEQ"); + + // dispatch untagged responses + notifyResponseHandlers(r); + + Response response = r[r.length-1]; + if (response.isOK()) + return FetchResponse.getItem(r, msgno, MODSEQ.class); + else if (response.isNO()) // XXX: Issue NOOP ? + return null; + else { + handleResult(response); + return null; // NOTREACHED + } + } + + /** + * Get the sequence number for the given UID. Nothing is returned; + * the FETCH UID response must be handled by the reponse handler, + * along with any possible EXPUNGE responses, to ensure that the + * UID is matched with the correct sequence number. + * + * @param uid the UID + * @exception ProtocolException for protocol failures + * @since JavaMail 1.5.3 + */ + public void fetchSequenceNumber(long uid) throws ProtocolException { + Response[] r = fetch(String.valueOf(uid), "UID", true); + + notifyResponseHandlers(r); + handleResult(r[r.length-1]); + } + + /** + * Get the sequence numbers for UIDs ranging from start till end. + * Since the range may be large and sparse, an array of the UIDs actually + * found is returned. The caller must map these to messages after + * the FETCH UID responses have been handled by the reponse handler, + * along with any possible EXPUNGE responses, to ensure that the + * UIDs are matched with the correct sequence numbers. + * + * @param start first UID + * @param end last UID + * @return array of sequence numbers + * @exception ProtocolException for protocol failures + * @since JavaMail 1.5.3 + */ + public long[] fetchSequenceNumbers(long start, long end) + throws ProtocolException { + Response[] r = fetch(String.valueOf(start) + ":" + + (end == UIDFolder.LASTUID ? "*" : + String.valueOf(end)), + "UID", true); + + UID u; + List v = new ArrayList<>(); + for (int i = 0, len = r.length; i < len; i++) { + if (r[i] == null || !(r[i] instanceof FetchResponse)) + continue; + + FetchResponse fr = (FetchResponse)r[i]; + if ((u = fr.getItem(UID.class)) != null) + v.add(u); + } + + notifyResponseHandlers(r); + handleResult(r[r.length-1]); + + long[] lv = new long[v.size()]; + for (int i = 0; i < v.size(); i++) + lv[i] = v.get(i).uid; + return lv; + } + + /** + * Get the sequence numbers for UIDs specified in the array. + * Nothing is returned. The caller must map the UIDs to messages after + * the FETCH UID responses have been handled by the reponse handler, + * along with any possible EXPUNGE responses, to ensure that the + * UIDs are matched with the correct sequence numbers. + * + * @param uids the UIDs + * @exception ProtocolException for protocol failures + * @since JavaMail 1.5.3 + */ + public void fetchSequenceNumbers(long[] uids) throws ProtocolException { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < uids.length; i++) { + if (i > 0) + sb.append(","); + sb.append(String.valueOf(uids[i])); + } + + Response[] r = fetch(sb.toString(), "UID", true); + + notifyResponseHandlers(r); + handleResult(r[r.length-1]); + } + + /** + * Get the sequence numbers for messages changed since the given + * modseq and with UIDs ranging from start till end. + * Also, prefetch the flags for the returned messages. + * + * @param start first UID + * @param end last UID + * @param modseq the MODSEQ + * @return array of sequence numbers + * @exception ProtocolException for protocol failures + * @see "RFC 4551" + * @since JavaMail 1.5.1 + */ + public int[] uidfetchChangedSince(long start, long end, long modseq) + throws ProtocolException { + String msgSequence = String.valueOf(start) + ":" + + (end == UIDFolder.LASTUID ? "*" : + String.valueOf(end)); + Response[] r = command("UID FETCH " + msgSequence + + " (FLAGS) (CHANGEDSINCE " + String.valueOf(modseq) + ")", null); + + List v = new ArrayList<>(); + for (int i = 0, len = r.length; i < len; i++) { + if (r[i] == null || !(r[i] instanceof FetchResponse)) + continue; + + FetchResponse fr = (FetchResponse)r[i]; + v.add(Integer.valueOf(fr.getNumber())); + } + + notifyResponseHandlers(r); + handleResult(r[r.length-1]); + + // Copy the list into 'matches' + int vsize = v.size(); + int[] matches = new int[vsize]; + for (int i = 0; i < vsize; i++) + matches[i] = v.get(i).intValue(); + return matches; + } + + public Response[] fetch(MessageSet[] msgsets, String what) + throws ProtocolException { + return fetch(MessageSet.toString(msgsets), what, false); + } + + public Response[] fetch(int start, int end, String what) + throws ProtocolException { + return fetch(String.valueOf(start) + ":" + String.valueOf(end), + what, false); + } + + public Response[] fetch(int msg, String what) + throws ProtocolException { + return fetch(String.valueOf(msg), what, false); + } + + private Response[] fetch(String msgSequence, String what, boolean uid) + throws ProtocolException { + if (uid) + return command("UID FETCH " + msgSequence +" (" + what + ")",null); + else + return command("FETCH " + msgSequence + " (" + what + ")", null); + } + + /** + * COPY command. + * + * @param msgsets the messages to copy + * @param mbox the mailbox to copy them to + * @exception ProtocolException for protocol failures + */ + public void copy(MessageSet[] msgsets, String mbox) + throws ProtocolException { + copyuid(MessageSet.toString(msgsets), mbox, false); + } + + /** + * COPY command. + * + * @param start start message number + * @param end end message number + * @param mbox the mailbox to copy them to + * @exception ProtocolException for protocol failures + */ + public void copy(int start, int end, String mbox) + throws ProtocolException { + copyuid(String.valueOf(start) + ":" + String.valueOf(end), + mbox, false); + } + + /** + * COPY command, return uid from COPYUID response code. + * + * @param msgsets the messages to copy + * @param mbox the mailbox to copy them to + * @return COPYUID response data + * @exception ProtocolException for protocol failures + * @see "RFC 4315, section 3" + */ + public CopyUID copyuid(MessageSet[] msgsets, String mbox) + throws ProtocolException { + return copyuid(MessageSet.toString(msgsets), mbox, true); + } + + /** + * COPY command, return uid from COPYUID response code. + * + * @param start start message number + * @param end end message number + * @param mbox the mailbox to copy them to + * @return COPYUID response data + * @exception ProtocolException for protocol failures + * @see "RFC 4315, section 3" + */ + public CopyUID copyuid(int start, int end, String mbox) + throws ProtocolException { + return copyuid(String.valueOf(start) + ":" + String.valueOf(end), + mbox, true); + } + + private CopyUID copyuid(String msgSequence, String mbox, boolean uid) + throws ProtocolException { + if (uid && !hasCapability("UIDPLUS")) + throw new BadCommandException("UIDPLUS not supported"); + + Argument args = new Argument(); + args.writeAtom(msgSequence); + writeMailboxName(args, mbox); + + Response[] r = command("COPY", args); + + // dispatch untagged responses + notifyResponseHandlers(r); + + // Handle result of this command + handleResult(r[r.length-1]); + + if (uid) + return getCopyUID(r); + else + return null; + } + + /** + * MOVE command. + * + * @param msgsets the messages to move + * @param mbox the mailbox to move them to + * @exception ProtocolException for protocol failures + * @see "RFC 6851" + * @since JavaMail 1.5.4 + */ + public void move(MessageSet[] msgsets, String mbox) + throws ProtocolException { + moveuid(MessageSet.toString(msgsets), mbox, false); + } + + /** + * MOVE command. + * + * @param start start message number + * @param end end message number + * @param mbox the mailbox to move them to + * @exception ProtocolException for protocol failures + * @see "RFC 6851" + * @since JavaMail 1.5.4 + */ + public void move(int start, int end, String mbox) + throws ProtocolException { + moveuid(String.valueOf(start) + ":" + String.valueOf(end), + mbox, false); + } + + /** + * MOVE Command, return uid from COPYUID response code. + * + * @param msgsets the messages to move + * @param mbox the mailbox to move them to + * @return COPYUID response data + * @exception ProtocolException for protocol failures + * @see "RFC 6851" + * @see "RFC 4315, section 3" + * @since JavaMail 1.5.4 + */ + public CopyUID moveuid(MessageSet[] msgsets, String mbox) + throws ProtocolException { + return moveuid(MessageSet.toString(msgsets), mbox, true); + } + + /** + * MOVE Command, return uid from COPYUID response code. + * + * @param start start message number + * @param end end message number + * @param mbox the mailbox to move them to + * @return COPYUID response data + * @exception ProtocolException for protocol failures + * @see "RFC 6851" + * @see "RFC 4315, section 3" + * @since JavaMail 1.5.4 + */ + public CopyUID moveuid(int start, int end, String mbox) + throws ProtocolException { + return moveuid(String.valueOf(start) + ":" + String.valueOf(end), + mbox, true); + } + + /** + * MOVE Command, return uid from COPYUID response code. + * + * @see "RFC 6851" + * @see "RFC 4315, section 3" + * @since JavaMail 1.5.4 + */ + private CopyUID moveuid(String msgSequence, String mbox, boolean uid) + throws ProtocolException { + if (!hasCapability("MOVE")) + throw new BadCommandException("MOVE not supported"); + if (uid && !hasCapability("UIDPLUS")) + throw new BadCommandException("UIDPLUS not supported"); + + Argument args = new Argument(); + args.writeAtom(msgSequence); + writeMailboxName(args, mbox); + + Response[] r = command("MOVE", args); + + // dispatch untagged responses + notifyResponseHandlers(r); + + // Handle result of this command + handleResult(r[r.length-1]); + + if (uid) + return getCopyUID(r); + else + return null; + } + + /** + * If the response contains a COPYUID response code, extract + * it and return a CopyUID object with the information. + * + * @param rr the responses to examine + * @return the COPYUID response code data, or null if not found + * @since JavaMail 1.5.4 + */ + protected CopyUID getCopyUID(Response[] rr) { + // most likely in the last response, so start there and work backward + for (int i = rr.length - 1; i >= 0; i--) { + Response r = rr[i]; + if (r == null || !r.isOK()) + continue; + byte b; + while ((b = r.readByte()) > 0 && b != (byte)'[') + ; + if (b == 0) + continue; + String s; + s = r.readAtom(); + if (!s.equalsIgnoreCase("COPYUID")) + continue; + + // XXX - need to merge more than one response for MOVE? + long uidvalidity = r.readLong(); + String src = r.readAtom(); + String dst = r.readAtom(); + return new CopyUID(uidvalidity, + UIDSet.parseUIDSets(src), UIDSet.parseUIDSets(dst)); + } + return null; + } + + public void storeFlags(MessageSet[] msgsets, Flags flags, boolean set) + throws ProtocolException { + storeFlags(MessageSet.toString(msgsets), flags, set); + } + + public void storeFlags(int start, int end, Flags flags, boolean set) + throws ProtocolException { + storeFlags(String.valueOf(start) + ":" + String.valueOf(end), + flags, set); + } + + /** + * Set the specified flags on this message. + * + * @param msg the message number + * @param flags the flags + * @param set true to set, false to clear + * @exception ProtocolException for protocol failures + */ + public void storeFlags(int msg, Flags flags, boolean set) + throws ProtocolException { + storeFlags(String.valueOf(msg), flags, set); + } + + private void storeFlags(String msgset, Flags flags, boolean set) + throws ProtocolException { + Response[] r; + if (set) + r = command("STORE " + msgset + " +FLAGS " + + createFlagList(flags), null); + else + r = command("STORE " + msgset + " -FLAGS " + + createFlagList(flags), null); + + // Dispatch untagged responses + notifyResponseHandlers(r); + handleResult(r[r.length-1]); + } + + /** + * Creates an IMAP flag_list from the given Flags object. + * + * @param flags the flags + * @return the IMAP flag_list + * @since JavaMail 1.5.4 + */ + protected String createFlagList(Flags flags) { + StringBuilder sb = new StringBuilder("("); // start of flag_list + + Flags.Flag[] sf = flags.getSystemFlags(); // get the system flags + boolean first = true; + for (int i = 0; i < sf.length; i++) { + String s; + Flags.Flag f = sf[i]; + if (f == Flags.Flag.ANSWERED) + s = "\\Answered"; + else if (f == Flags.Flag.DELETED) + s = "\\Deleted"; + else if (f == Flags.Flag.DRAFT) + s = "\\Draft"; + else if (f == Flags.Flag.FLAGGED) + s = "\\Flagged"; + else if (f == Flags.Flag.RECENT) + s = "\\Recent"; + else if (f == Flags.Flag.SEEN) + s = "\\Seen"; + else + continue; // skip it + if (first) + first = false; + else + sb.append(' '); + sb.append(s); + } + + String[] uf = flags.getUserFlags(); // get the user flag strings + for (int i = 0; i < uf.length; i++) { + if (first) + first = false; + else + sb.append(' '); + sb.append(uf[i]); + } + + sb.append(")"); // terminate flag_list + return sb.toString(); + } + + /** + * Issue the given search criterion on the specified message sets. + * Returns array of matching sequence numbers. An empty array + * is returned if no matches are found. + * + * @param msgsets array of MessageSets + * @param term SearchTerm + * @return array of matching sequence numbers. + * @exception ProtocolException for protocol failures + * @exception SearchException for search failures + */ + public int[] search(MessageSet[] msgsets, SearchTerm term) + throws ProtocolException, SearchException { + return search(MessageSet.toString(msgsets), term); + } + + /** + * Issue the given search criterion on all messages in this folder. + * Returns array of matching sequence numbers. An empty array + * is returned if no matches are found. + * + * @param term SearchTerm + * @return array of matching sequence numbers. + * @exception ProtocolException for protocol failures + * @exception SearchException for search failures + */ + public int[] search(SearchTerm term) + throws ProtocolException, SearchException { + return search("ALL", term); + } + + /* + * Apply the given SearchTerm on the specified sequence. + * Returns array of matching sequence numbers. Note that an empty + * array is returned for no matches. + */ + private int[] search(String msgSequence, SearchTerm term) + throws ProtocolException, SearchException { + // Check if the search "text" terms contain only ASCII chars, + // or if utf8 support has been enabled (in which case CHARSET + // is not allowed; see RFC 6855, section 3, last paragraph) + if (supportsUtf8() || SearchSequence.isAscii(term)) { + try { + return issueSearch(msgSequence, term, null); + } catch (IOException ioex) { /* will not happen */ } + } + + /* + * The search "text" terms do contain non-ASCII chars and utf8 + * support has not been enabled. We need to use: + * "SEARCH CHARSET ..." + * The charsets we try to use are UTF-8 and the locale's + * default charset. If the server supports UTF-8, great, + * always use it. Else we try to use the default charset. + */ + + // Cycle thru the list of charsets + for (int i = 0; i < searchCharsets.length; i++) { + if (searchCharsets[i] == null) + continue; + + try { + return issueSearch(msgSequence, term, searchCharsets[i]); + } catch (CommandFailedException cfx) { + /* + * Server returned NO. For now, I'll just assume that + * this indicates that this charset is unsupported. + * We can check the BADCHARSET response code once + * that's spec'd into the IMAP RFC .. + */ + searchCharsets[i] = null; + continue; + } catch (IOException ioex) { + /* Charset conversion failed. Try the next one */ + continue; + } catch (ProtocolException pex) { + throw pex; + } catch (SearchException sex) { + throw sex; + } + } + + // No luck. + throw new SearchException("Search failed"); + } + + /* Apply the given SearchTerm on the specified sequence, using the + * given charset.

+ * Returns array of matching sequence numbers. Note that an empty + * array is returned for no matches. + */ + private int[] issueSearch(String msgSequence, SearchTerm term, + String charset) + throws ProtocolException, SearchException, IOException { + + // Generate a search-sequence with the given charset + Argument args = getSearchSequence().generateSequence(term, + charset == null ? null : + MimeUtility.javaCharset(charset) + ); + args.writeAtom(msgSequence); + + Response[] r; + + if (charset == null) // text is all US-ASCII + r = command("SEARCH", args); + else + r = command("SEARCH CHARSET " + charset, args); + + Response response = r[r.length-1]; + int[] matches = null; + + // Grab all SEARCH responses + if (response.isOK()) { // command succesful + List v = new ArrayList<>(); + int num; + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + // There *will* be one SEARCH response. + if (ir.keyEquals("SEARCH")) { + while ((num = ir.readNumber()) != -1) + v.add(Integer.valueOf(num)); + r[i] = null; + } + } + + // Copy the list into 'matches' + int vsize = v.size(); + matches = new int[vsize]; + for (int i = 0; i < vsize; i++) + matches[i] = v.get(i).intValue(); + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return matches; + } + + /** + * Get the SearchSequence object. + * The SearchSequence object instance is saved in the searchSequence + * field. Subclasses of IMAPProtocol may override this method to + * return a subclass of SearchSequence, in order to add support for + * product-specific search terms. + * + * @return the SearchSequence + * @since JavaMail 1.4.6 + */ + protected SearchSequence getSearchSequence() { + if (searchSequence == null) + searchSequence = new SearchSequence(this); + return searchSequence; + } + + /** + * Sort messages in the folder according to the specified sort criteria. + * If the search term is not null, limit the sort to only the messages + * that match the search term. + * Returns an array of sorted sequence numbers. An empty array + * is returned if no matches are found. + * + * @param term sort criteria + * @param sterm SearchTerm + * @return array of matching sequence numbers. + * @exception ProtocolException for protocol failures + * @exception SearchException for search failures + * + * @see "RFC 5256" + * @since JavaMail 1.4.4 + */ + public int[] sort(SortTerm[] term, SearchTerm sterm) + throws ProtocolException, SearchException { + if (!hasCapability("SORT*")) + throw new BadCommandException("SORT not supported"); + + if (term == null || term.length == 0) + throw new BadCommandException("Must have at least one sort term"); + + Argument args = new Argument(); + Argument sargs = new Argument(); + for (int i = 0; i < term.length; i++) + sargs.writeAtom(term[i].toString()); + args.writeArgument(sargs); // sort criteria + + args.writeAtom("UTF-8"); // charset specification + if (sterm != null) { + try { + args.append( + getSearchSequence().generateSequence(sterm, "UTF-8")); + } catch (IOException ioex) { + // should never happen + throw new SearchException(ioex.toString()); + } + } else + args.writeAtom("ALL"); + + Response[] r = command("SORT", args); + Response response = r[r.length-1]; + int[] matches = null; + + // Grab all SORT responses + if (response.isOK()) { // command succesful + List v = new ArrayList<>(); + int num; + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("SORT")) { + while ((num = ir.readNumber()) != -1) + v.add(Integer.valueOf(num)); + r[i] = null; + } + } + + // Copy the list into 'matches' + int vsize = v.size(); + matches = new int[vsize]; + for (int i = 0; i < vsize; i++) + matches[i] = v.get(i).intValue(); + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return matches; + } + + /** + * NAMESPACE Command. + * + * @return the namespaces + * @exception ProtocolException for protocol failures + * @see "RFC2342" + */ + public Namespaces namespace() throws ProtocolException { + if (!hasCapability("NAMESPACE")) + throw new BadCommandException("NAMESPACE not supported"); + + Response[] r = command("NAMESPACE", null); + + Namespaces namespace = null; + Response response = r[r.length-1]; + + // Grab NAMESPACE response + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("NAMESPACE")) { + if (namespace == null) + namespace = new Namespaces(ir); + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return namespace; + } + + /** + * GETQUOTAROOT Command. + * + * Returns an array of Quota objects, representing the quotas + * for this mailbox and, indirectly, the quotaroots for this + * mailbox. + * + * @param mbox the mailbox + * @return array of Quota objects + * @exception ProtocolException for protocol failures + * @see "RFC2087" + */ + public Quota[] getQuotaRoot(String mbox) throws ProtocolException { + if (!hasCapability("QUOTA")) + throw new BadCommandException("GETQUOTAROOT not supported"); + + Argument args = new Argument(); + writeMailboxName(args, mbox); + + Response[] r = command("GETQUOTAROOT", args); + + Response response = r[r.length-1]; + + Map tab = new HashMap<>(); + + // Grab all QUOTAROOT and QUOTA responses + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("QUOTAROOT")) { + // quotaroot_response + // ::= "QUOTAROOT" SP astring *(SP astring) + + // read name of mailbox and throw away + ir.readAtomString(); + // for each quotaroot add a placeholder quota + String root = null; + while ((root = ir.readAtomString()) != null && + root.length() > 0) + tab.put(root, new Quota(root)); + r[i] = null; + } else if (ir.keyEquals("QUOTA")) { + Quota quota = parseQuota(ir); + Quota q = tab.get(quota.quotaRoot); + if (q != null && q.resources != null) { + // merge resources + int newl = q.resources.length + quota.resources.length; + Quota.Resource[] newr = new Quota.Resource[newl]; + System.arraycopy(q.resources, 0, newr, 0, + q.resources.length); + System.arraycopy(quota.resources, 0, + newr, q.resources.length, quota.resources.length); + quota.resources = newr; + } + tab.put(quota.quotaRoot, quota); + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + + return tab.values().toArray(new Quota[tab.size()]); + } + + /** + * GETQUOTA Command. + * + * Returns an array of Quota objects, representing the quotas + * for this quotaroot. + * + * @param root the quotaroot + * @return the quotas + * @exception ProtocolException for protocol failures + * @see "RFC2087" + */ + public Quota[] getQuota(String root) throws ProtocolException { + if (!hasCapability("QUOTA")) + throw new BadCommandException("QUOTA not supported"); + + Argument args = new Argument(); + args.writeString(root); // XXX - could be UTF-8? + + Response[] r = command("GETQUOTA", args); + + Quota quota = null; + List v = new ArrayList<>(); + Response response = r[r.length-1]; + + // Grab all QUOTA responses + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("QUOTA")) { + quota = parseQuota(ir); + v.add(quota); + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return v.toArray(new Quota[v.size()]); + } + + /** + * SETQUOTA Command. + * + * Set the indicated quota on the corresponding quotaroot. + * + * @param quota the quota to set + * @exception ProtocolException for protocol failures + * @see "RFC2087" + */ + public void setQuota(Quota quota) throws ProtocolException { + if (!hasCapability("QUOTA")) + throw new BadCommandException("QUOTA not supported"); + + Argument args = new Argument(); + args.writeString(quota.quotaRoot); // XXX - could be UTF-8? + Argument qargs = new Argument(); + if (quota.resources != null) { + for (int i = 0; i < quota.resources.length; i++) { + qargs.writeAtom(quota.resources[i].name); + qargs.writeNumber(quota.resources[i].limit); + } + } + args.writeArgument(qargs); + + Response[] r = command("SETQUOTA", args); + Response response = r[r.length-1]; + + // XXX - It's not clear from the RFC whether the SETQUOTA command + // will provoke untagged QUOTA responses. If it does, perhaps + // we should grab them here and return them? + + /* + Quota quota = null; + List v = new ArrayList(); + + // Grab all QUOTA responses + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("QUOTA")) { + quota = parseQuota(ir); + v.add(quota); + r[i] = null; + } + } + } + */ + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + /* + return v.toArray(new Quota[v.size()]); + */ + } + + /** + * Parse a QUOTA response. + */ + private Quota parseQuota(Response r) throws ParsingException { + // quota_response ::= "QUOTA" SP astring SP quota_list + String quotaRoot = r.readAtomString(); // quotaroot ::= astring + Quota q = new Quota(quotaRoot); + r.skipSpaces(); + // quota_list ::= "(" #quota_resource ")" + if (r.readByte() != '(') + throw new ParsingException("parse error in QUOTA"); + + List v = new ArrayList<>(); + while (!r.isNextNonSpace(')')) { + // quota_resource ::= atom SP number SP number + String name = r.readAtom(); + if (name != null) { + long usage = r.readLong(); + long limit = r.readLong(); + Quota.Resource res = new Quota.Resource(name, usage, limit); + v.add(res); + } + } + q.resources = v.toArray(new Quota.Resource[v.size()]); + return q; + } + + + /** + * SETACL Command. + * + * @param mbox the mailbox + * @param modifier the ACL modifier + * @param acl the ACL + * @exception ProtocolException for protocol failures + * @see "RFC2086" + */ + public void setACL(String mbox, char modifier, ACL acl) + throws ProtocolException { + if (!hasCapability("ACL")) + throw new BadCommandException("ACL not supported"); + + Argument args = new Argument(); + writeMailboxName(args, mbox); + args.writeString(acl.getName()); + String rights = acl.getRights().toString(); + if (modifier == '+' || modifier == '-') + rights = modifier + rights; + args.writeString(rights); + + Response[] r = command("SETACL", args); + Response response = r[r.length-1]; + + // dispatch untagged responses + notifyResponseHandlers(r); + handleResult(response); + } + + /** + * DELETEACL Command. + * + * @param mbox the mailbox + * @param user the user + * @exception ProtocolException for protocol failures + * @see "RFC2086" + */ + public void deleteACL(String mbox, String user) throws ProtocolException { + if (!hasCapability("ACL")) + throw new BadCommandException("ACL not supported"); + + Argument args = new Argument(); + writeMailboxName(args, mbox); + args.writeString(user); // XXX - could be UTF-8? + + Response[] r = command("DELETEACL", args); + Response response = r[r.length-1]; + + // dispatch untagged responses + notifyResponseHandlers(r); + handleResult(response); + } + + /** + * GETACL Command. + * + * @param mbox the mailbox + * @return the ACL array + * @exception ProtocolException for protocol failures + * @see "RFC2086" + */ + public ACL[] getACL(String mbox) throws ProtocolException { + if (!hasCapability("ACL")) + throw new BadCommandException("ACL not supported"); + + Argument args = new Argument(); + writeMailboxName(args, mbox); + + Response[] r = command("GETACL", args); + Response response = r[r.length-1]; + + // Grab all ACL responses + List v = new ArrayList<>(); + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("ACL")) { + // acl_data ::= "ACL" SPACE mailbox + // *(SPACE identifier SPACE rights) + // read name of mailbox and throw away + ir.readAtomString(); + String name = null; + while ((name = ir.readAtomString()) != null) { + String rights = ir.readAtomString(); + if (rights == null) + break; + ACL acl = new ACL(name, new Rights(rights)); + v.add(acl); + } + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return v.toArray(new ACL[v.size()]); + } + + /** + * LISTRIGHTS Command. + * + * @param mbox the mailbox + * @param user the user rights to return + * @return the rights array + * @exception ProtocolException for protocol failures + * @see "RFC2086" + */ + public Rights[] listRights(String mbox, String user) + throws ProtocolException { + if (!hasCapability("ACL")) + throw new BadCommandException("ACL not supported"); + + Argument args = new Argument(); + writeMailboxName(args, mbox); + args.writeString(user); // XXX - could be UTF-8? + + Response[] r = command("LISTRIGHTS", args); + Response response = r[r.length-1]; + + // Grab LISTRIGHTS response + List v = new ArrayList<>(); + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("LISTRIGHTS")) { + // listrights_data ::= "LISTRIGHTS" SPACE mailbox + // SPACE identifier SPACE rights *(SPACE rights) + // read name of mailbox and throw away + ir.readAtomString(); + // read identifier and throw away + ir.readAtomString(); + String rights; + while ((rights = ir.readAtomString()) != null) + v.add(new Rights(rights)); + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return v.toArray(new Rights[v.size()]); + } + + /** + * MYRIGHTS Command. + * + * @param mbox the mailbox + * @return the rights + * @exception ProtocolException for protocol failures + * @see "RFC2086" + */ + public Rights myRights(String mbox) throws ProtocolException { + if (!hasCapability("ACL")) + throw new BadCommandException("ACL not supported"); + + Argument args = new Argument(); + writeMailboxName(args, mbox); + + Response[] r = command("MYRIGHTS", args); + Response response = r[r.length-1]; + + // Grab MYRIGHTS response + Rights rights = null; + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("MYRIGHTS")) { + // myrights_data ::= "MYRIGHTS" SPACE mailbox SPACE rights + // read name of mailbox and throw away + ir.readAtomString(); + String rs = ir.readAtomString(); + if (rights == null) + rights = new Rights(rs); + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return rights; + } + + /* + * The tag used on the IDLE command. Set by idleStart() and + * used in processIdleResponse() to determine if the response + * is the matching end tag. + */ + private volatile String idleTag; + + /** + * IDLE Command.

+ * + * If the server supports the IDLE command extension, the IDLE + * command is issued and this method blocks until a response has + * been received. Once the first response has been received, the + * IDLE command is terminated and all responses are collected and + * handled and this method returns.

+ * + * Note that while this method is blocked waiting for a response, + * no other threads may issue any commands to the server that would + * use this same connection. + * + * @exception ProtocolException for protocol failures + * @see "RFC2177" + * @since JavaMail 1.4.1 + */ + public synchronized void idleStart() throws ProtocolException { + if (!hasCapability("IDLE")) + throw new BadCommandException("IDLE not supported"); + + List v = new ArrayList<>(); + boolean done = false; + Response r = null; + + // write the command + try { + idleTag = writeCommand("IDLE", null); + } catch (LiteralException lex) { + v.add(lex.getResponse()); + done = true; + } catch (Exception ex) { + // Convert this into a BYE response + v.add(Response.byeResponse(ex)); + done = true; + } + + while (!done) { + try { + r = readResponse(); + } catch (IOException ioex) { + // convert this into a BYE response + r = Response.byeResponse(ioex); + } catch (ProtocolException pex) { + continue; // skip this response + } + + v.add(r); + + if (r.isContinuation() || r.isBYE()) + done = true; + } + + Response[] responses = v.toArray(new Response[v.size()]); + r = responses[responses.length-1]; + + // dispatch remaining untagged responses + notifyResponseHandlers(responses); + if (!r.isContinuation()) + handleResult(r); + } + + /** + * While an IDLE command is in progress, read a response + * sent from the server. The response is read with no locks + * held so that when the read blocks waiting for the response + * from the server it's not holding locks that would prevent + * other threads from interrupting the IDLE command. + * + * @return the response + * @since JavaMail 1.4.1 + */ + public synchronized Response readIdleResponse() { + if (idleTag == null) + return null; // IDLE not in progress + Response r = null; + try { + r = readResponse(); + } catch (IOException ioex) { + // convert this into a BYE response + r = Response.byeResponse(ioex); + } catch (ProtocolException pex) { + // convert this into a BYE response + r = Response.byeResponse(pex); + } + return r; + } + + /** + * Process a response returned by readIdleResponse(). + * This method will be called with appropriate locks + * held so that the processing of the response is safe. + * + * @param r the response + * @return true if IDLE is done + * @exception ProtocolException for protocol failures + * @since JavaMail 1.4.1 + */ + public boolean processIdleResponse(Response r) throws ProtocolException { + Response[] responses = new Response[1]; + responses[0] = r; + boolean done = false; // done reading responses? + notifyResponseHandlers(responses); + + if (r.isBYE()) // shouldn't wait for command completion response + done = true; + + // If this is a matching command completion response, we are done + if (r.isTagged() && r.getTag().equals(idleTag)) + done = true; + + if (done) + idleTag = null; // no longer in IDLE + + handleResult(r); + return !done; + } + + // the DONE command to break out of IDLE + private static final byte[] DONE = { 'D', 'O', 'N', 'E', '\r', '\n' }; + + /** + * Abort an IDLE command. While one thread is blocked in + * readIdleResponse(), another thread will use this method + * to abort the IDLE command, which will cause the server + * to send the closing tag for the IDLE command, which + * readIdleResponse() and processIdleResponse() will see + * and terminate the IDLE state. + * + * @since JavaMail 1.4.1 + */ + public void idleAbort() { + OutputStream os = getOutputStream(); + try { + os.write(DONE); + os.flush(); + } catch (Exception ex) { + // nothing to do, hope to detect it again later + logger.log(Level.FINEST, "Exception aborting IDLE", ex); + } + } + + /** + * ID Command. + * + * @param clientParams map of names and values + * @return map of names and values from server + * @exception ProtocolException for protocol failures + * @see "RFC 2971" + * @since JavaMail 1.5.1 + */ + public Map id(Map clientParams) + throws ProtocolException { + if (!hasCapability("ID")) + throw new BadCommandException("ID not supported"); + + Response[] r = command("ID", ID.getArgumentList(clientParams)); + + ID id = null; + Response response = r[r.length-1]; + + // Grab ID response + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("ID")) { + if (id == null) + id = new ID(ir); + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return id == null ? null : id.getServerParams(); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/IMAPReferralException.java b/app/src/main/java/com/sun/mail/imap/protocol/IMAPReferralException.java new file mode 100644 index 0000000000..82f1ea39d1 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/IMAPReferralException.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import com.sun.mail.iap.ProtocolException; + +/** + * A ProtocolException that includes IMAP login referral information. + * + * @since JavaMail 1.5.5 + */ + +public class IMAPReferralException extends ProtocolException { + + private String url; + + private static final long serialVersionUID = 2578770669364251968L; + + /** + * Constructs an IMAPReferralException with the specified detail message. + * and URL. + * + * @param s the detail message + * @param url the URL + */ + public IMAPReferralException(String s, String url) { + super(s); + this.url = url; + } + + /** + * Return the IMAP URL in the referral. + * + * @return the IMAP URL + */ + public String getUrl() { + return url; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/IMAPResponse.java b/app/src/main/java/com/sun/mail/imap/protocol/IMAPResponse.java new file mode 100644 index 0000000000..4e6be4e1b3 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/IMAPResponse.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.io.*; +import java.util.*; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.iap.*; + +/** + * This class represents a response obtained from the input stream + * of an IMAP server. + * + * @author John Mani + */ + +public class IMAPResponse extends Response { + private String key; + private int number; + + public IMAPResponse(Protocol c) throws IOException, ProtocolException { + super(c); + init(); + } + + private void init() throws IOException, ProtocolException { + // continue parsing if this is an untagged response + if (isUnTagged() && !isOK() && !isNO() && !isBAD() && !isBYE()) { + key = readAtom(); + + // Is this response of the form "* " + try { + number = Integer.parseInt(key); + key = readAtom(); + } catch (NumberFormatException ne) { } + } + } + + /** + * Copy constructor. + * + * @param r the IMAPResponse to copy + */ + public IMAPResponse(IMAPResponse r) { + super((Response)r); + key = r.key; + number = r.number; + } + + /** + * For testing. + * + * @param r the response string + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + public IMAPResponse(String r) throws IOException, ProtocolException { + this(r, true); + } + + /** + * For testing. + * + * @param r the response string + * @param utf8 UTF-8 allowed? + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + * @since JavaMail 1.6.0 + */ + public IMAPResponse(String r, boolean utf8) + throws IOException, ProtocolException { + super(r, utf8); + init(); + } + + /** + * Read a list of space-separated "flag-extension" sequences and + * return the list as a array of Strings. An empty list is returned + * as null. Each item is expected to be an atom, possibly preceeded + * by a backslash, but we aren't that strict; we just look for strings + * separated by spaces and terminated by a right paren. We assume items + * are always ASCII. + * + * @return the list items as a String array + */ + public String[] readSimpleList() { + skipSpaces(); + + if (buffer[index] != '(') // not what we expected + return null; + index++; // skip '(' + + List v = new ArrayList<>(); + int start; + for (start = index; buffer[index] != ')'; index++) { + if (buffer[index] == ' ') { // got one item + v.add(ASCIIUtility.toString(buffer, start, index)); + start = index+1; // index gets incremented at the top + } + } + if (index > start) // get the last item + v.add(ASCIIUtility.toString(buffer, start, index)); + index++; // skip ')' + + int size = v.size(); + if (size > 0) + return v.toArray(new String[size]); + else // empty list + return null; + } + + public String getKey() { + return key; + } + + public boolean keyEquals(String k) { + if (key != null && key.equalsIgnoreCase(k)) + return true; + else + return false; + } + + public int getNumber() { + return number; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/INTERNALDATE.java b/app/src/main/java/com/sun/mail/imap/protocol/INTERNALDATE.java new file mode 100644 index 0000000000..7f432ff51c --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/INTERNALDATE.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.Date; +import java.util.TimeZone; +import java.util.Locale; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.text.FieldPosition; + +import javax.mail.internet.MailDateFormat; + +import com.sun.mail.iap.*; + + +/** + * An INTERNALDATE FETCH item. + * + * @author John Mani + */ + +public class INTERNALDATE implements Item { + + static final char[] name = + {'I','N','T','E','R','N','A','L','D','A','T','E'}; + public int msgno; + protected Date date; + + /* + * Used to parse dates only. The parse method is thread safe + * so we only need to create a single object for use by all + * instances. We depend on the fact that the MailDateFormat + * class will parse dates in INTERNALDATE format as well as + * dates in RFC 822 format. + */ + private static final MailDateFormat mailDateFormat = new MailDateFormat(); + + /** + * Constructor. + * + * @param r the FetchResponse + * @exception ParsingException for parsing failures + */ + public INTERNALDATE(FetchResponse r) throws ParsingException { + msgno = r.getNumber(); + r.skipSpaces(); + String s = r.readString(); + if (s == null) + throw new ParsingException("INTERNALDATE is NIL"); + try { + synchronized (mailDateFormat) { + date = mailDateFormat.parse(s); + } + } catch (ParseException pex) { + throw new ParsingException("INTERNALDATE parse error"); + } + } + + public Date getDate() { + return date; + } + + // INTERNALDATE formatter + + private static SimpleDateFormat df = + // Need Locale.US, the "MMM" field can produce unexpected values + // in non US locales ! + new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss ", Locale.US); + + /** + * Format given Date object into INTERNALDATE string + * + * @param d the Date + * @return INTERNALDATE string + */ + public static String format(Date d) { + /* + * SimpleDateFormat objects aren't thread safe, so rather + * than create a separate such object for each request, + * we create one object and synchronize its use here + * so that only one thread is using it at a time. This + * trades off some potential concurrency for speed in the + * common case. + * + * This method is only used when formatting the date in a + * message that's being appended to a folder. + */ + StringBuffer sb = new StringBuffer(); + synchronized (df) { + df.format(d, sb, new FieldPosition(0)); + } + + // compute timezone offset string + TimeZone tz = TimeZone.getDefault(); + int offset = tz.getOffset(d.getTime()); // get offset from GMT + int rawOffsetInMins = offset / 60 / 1000; // offset from GMT in mins + if (rawOffsetInMins < 0) { + sb.append('-'); + rawOffsetInMins = (-rawOffsetInMins); + } else + sb.append('+'); + + int offsetInHrs = rawOffsetInMins / 60; + int offsetInMins = rawOffsetInMins % 60; + + sb.append(Character.forDigit((offsetInHrs/10), 10)); + sb.append(Character.forDigit((offsetInHrs%10), 10)); + sb.append(Character.forDigit((offsetInMins/10), 10)); + sb.append(Character.forDigit((offsetInMins%10), 10)); + + return sb.toString(); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/Item.java b/app/src/main/java/com/sun/mail/imap/protocol/Item.java new file mode 100644 index 0000000000..8d604ab939 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/Item.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +/** + * A tagging interface for all IMAP data items. + * Note that the "name" field of all IMAP items MUST be in uppercase.

+ * + * See the BODY, BODYSTRUCTURE, ENVELOPE, FLAGS, INTERNALDATE, RFC822DATA, + * RFC822SIZE, and UID classes. + * + * @author John Mani + */ + +public interface Item { +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/ListInfo.java b/app/src/main/java/com/sun/mail/imap/protocol/ListInfo.java new file mode 100644 index 0000000000..0db1f028e8 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/ListInfo.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.List; +import java.util.ArrayList; + +import com.sun.mail.iap.*; + +/** + * A LIST response. + * + * @author John Mani + * @author Bill Shannon + */ + +public class ListInfo { + public String name = null; + public char separator = '/'; + public boolean hasInferiors = true; + public boolean canOpen = true; + public int changeState = INDETERMINATE; + public String[] attrs; + + public static final int CHANGED = 1; + public static final int UNCHANGED = 2; + public static final int INDETERMINATE = 3; + + public ListInfo(IMAPResponse r) throws ParsingException { + String[] s = r.readSimpleList(); + + List v = new ArrayList<>(); // accumulate attributes + if (s != null) { + // non-empty attribute list + for (int i = 0; i < s.length; i++) { + if (s[i].equalsIgnoreCase("\\Marked")) + changeState = CHANGED; + else if (s[i].equalsIgnoreCase("\\Unmarked")) + changeState = UNCHANGED; + else if (s[i].equalsIgnoreCase("\\Noselect")) + canOpen = false; + else if (s[i].equalsIgnoreCase("\\Noinferiors")) + hasInferiors = false; + v.add(s[i]); + } + } + attrs = v.toArray(new String[v.size()]); + + r.skipSpaces(); + if (r.readByte() == '"') { + if ((separator = (char)r.readByte()) == '\\') + // escaped separator character + separator = (char)r.readByte(); + r.skip(1); // skip <"> + } else // NIL + r.skip(2); + + r.skipSpaces(); + name = r.readAtomString(); + + if (!r.supportsUtf8()) + // decode the name (using RFC2060's modified UTF7) + name = BASE64MailboxDecoder.decode(name); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/MODSEQ.java b/app/src/main/java/com/sun/mail/imap/protocol/MODSEQ.java new file mode 100644 index 0000000000..f800569309 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/MODSEQ.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import com.sun.mail.iap.*; + +/** + * This class represents the MODSEQ data item. + * + * @since JavaMail 1.5.1 + * @author Bill Shannon + */ + +public class MODSEQ implements Item { + + static final char[] name = {'M','O','D','S','E','Q'}; + public int seqnum; + + public long modseq; + + /** + * Constructor. + * + * @param r the FetchResponse + * @exception ParsingException for parsing failures + */ + public MODSEQ(FetchResponse r) throws ParsingException { + seqnum = r.getNumber(); + r.skipSpaces(); + + if (r.readByte() != '(') + throw new ParsingException("MODSEQ parse error"); + + modseq = r.readLong(); + + if (!r.isNextNonSpace(')')) + throw new ParsingException("MODSEQ parse error"); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/MailboxInfo.java b/app/src/main/java/com/sun/mail/imap/protocol/MailboxInfo.java new file mode 100644 index 0000000000..ccff0b6964 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/MailboxInfo.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.List; +import java.util.ArrayList; + +import javax.mail.Flags; + +import com.sun.mail.iap.*; + +/** + * Information collected when opening a mailbox. + * + * @author John Mani + * @author Bill Shannon + */ + +public class MailboxInfo { + /** The available flags. */ + public Flags availableFlags = null; + /** The permanent flags. */ + public Flags permanentFlags = null; + /** The total number of messages. */ + public int total = -1; + /** The number of recent messages. */ + public int recent = -1; + /** The first unseen message. */ + public int first = -1; + /** The UIDVALIDITY. */ + public long uidvalidity = -1; + /** The next UID value to be assigned. */ + public long uidnext = -1; + /** UIDs are not sticky. */ + public boolean uidNotSticky = false; // RFC 4315 + /** The highest MODSEQ value. */ + public long highestmodseq = -1; // RFC 4551 - CONDSTORE + /** Folder.READ_WRITE or Folder.READ_ONLY, set by IMAPProtocol. */ + public int mode; + /** VANISHED or FETCH responses received while opening the mailbox. */ + public List responses; + + /** + * Collect the information about this mailbox from the + * responses to a SELECT or EXAMINE. + * + * @param r the responses + * @exception ParsingException for errors parsing the responses + */ + public MailboxInfo(Response[] r) throws ParsingException { + for (int i = 0; i < r.length; i++) { + if (r[i] == null || !(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + + if (ir.keyEquals("EXISTS")) { + total = ir.getNumber(); + r[i] = null; // remove this response + } else if (ir.keyEquals("RECENT")) { + recent = ir.getNumber(); + r[i] = null; // remove this response + } else if (ir.keyEquals("FLAGS")) { + availableFlags = new FLAGS(ir); + r[i] = null; // remove this response + } else if (ir.keyEquals("VANISHED")) { + if (responses == null) + responses = new ArrayList<>(); + responses.add(ir); + r[i] = null; // remove this response + } else if (ir.keyEquals("FETCH")) { + if (responses == null) + responses = new ArrayList<>(); + responses.add(ir); + r[i] = null; // remove this response + } else if (ir.isUnTagged() && ir.isOK()) { + /* + * should be one of: + * * OK [UNSEEN 12] + * * OK [UIDVALIDITY 3857529045] + * * OK [PERMANENTFLAGS (\Deleted)] + * * OK [UIDNEXT 44] + * * OK [HIGHESTMODSEQ 103] + */ + ir.skipSpaces(); + + if (ir.readByte() != '[') { // huh ??? + ir.reset(); + continue; + } + + boolean handled = true; + String s = ir.readAtom(); + if (s.equalsIgnoreCase("UNSEEN")) + first = ir.readNumber(); + else if (s.equalsIgnoreCase("UIDVALIDITY")) + uidvalidity = ir.readLong(); + else if (s.equalsIgnoreCase("PERMANENTFLAGS")) + permanentFlags = new FLAGS(ir); + else if (s.equalsIgnoreCase("UIDNEXT")) + uidnext = ir.readLong(); + else if (s.equalsIgnoreCase("HIGHESTMODSEQ")) + highestmodseq = ir.readLong(); + else + handled = false; // possibly an ALERT + + if (handled) + r[i] = null; // remove this response + else + ir.reset(); // so ALERT can be read + } else if (ir.isUnTagged() && ir.isNO()) { + /* + * should be one of: + * * NO [UIDNOTSTICKY] + */ + ir.skipSpaces(); + + if (ir.readByte() != '[') { // huh ??? + ir.reset(); + continue; + } + + boolean handled = true; + String s = ir.readAtom(); + if (s.equalsIgnoreCase("UIDNOTSTICKY")) + uidNotSticky = true; + else + handled = false; // possibly an ALERT + + if (handled) + r[i] = null; // remove this response + else + ir.reset(); // so ALERT can be read + } + } + + /* + * The PERMANENTFLAGS response code is optional, and if + * not present implies that all flags in the required FLAGS + * response can be changed permanently. + */ + if (permanentFlags == null) { + if (availableFlags != null) + permanentFlags = new Flags(availableFlags); + else + permanentFlags = new Flags(); + } + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/MessageSet.java b/app/src/main/java/com/sun/mail/imap/protocol/MessageSet.java new file mode 100644 index 0000000000..a7e6cc6b5b --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/MessageSet.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.List; +import java.util.ArrayList; + +/** + * This class holds the 'start' and 'end' for a range of messages. + */ +public class MessageSet { + + public int start; + public int end; + + public MessageSet() { } + + public MessageSet(int start, int end) { + this.start = start; + this.end = end; + } + + /** + * Count the total number of elements in a MessageSet + * + * @return how many messages in this MessageSet + */ + public int size() { + return end - start + 1; + } + + /** + * Convert an array of integers into an array of MessageSets + * + * @param msgs the messages + * @return array of MessageSet objects + */ + public static MessageSet[] createMessageSets(int[] msgs) { + List v = new ArrayList<>(); + int i,j; + + for (i=0; i < msgs.length; i++) { + MessageSet ms = new MessageSet(); + ms.start = msgs[i]; + + // Look for contiguous elements + for (j=i+1; j < msgs.length; j++) { + if (msgs[j] != msgs[j-1] +1) + break; + } + ms.end = msgs[j-1]; + v.add(ms); + i = j-1; // i gets incremented @ top of the loop + } + return v.toArray(new MessageSet[v.size()]); + } + + /** + * Convert an array of MessageSets into an IMAP sequence range + * + * @param msgsets the MessageSets + * @return IMAP sequence string + */ + public static String toString(MessageSet[] msgsets) { + if (msgsets == null || msgsets.length == 0) // Empty msgset + return null; + + int i = 0; // msgset index + StringBuilder s = new StringBuilder(); + int size = msgsets.length; + int start, end; + + for (;;) { + start = msgsets[i].start; + end = msgsets[i].end; + + if (end > start) + s.append(start).append(':').append(end); + else // end == start means only one element + s.append(start); + + i++; // Next MessageSet + if (i >= size) // No more MessageSets + break; + else + s.append(','); + } + return s.toString(); + } + + + /* + * Count the total number of elements in an array of MessageSets + */ + public static int size(MessageSet[] msgsets) { + int count = 0; + + if (msgsets == null) // Null msgset + return 0; + + for (int i=0; i < msgsets.length; i++) + count += msgsets[i].size(); + + return count; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/Namespaces.java b/app/src/main/java/com/sun/mail/imap/protocol/Namespaces.java new file mode 100644 index 0000000000..df54e67e2c --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/Namespaces.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.*; +import com.sun.mail.iap.*; + +/** + * This class and its inner class represent the response to the + * NAMESPACE command.

+ * + * See RFC 2342. + * + * @author Bill Shannon + */ + +public class Namespaces { + + /** + * A single namespace entry. + */ + public static class Namespace { + /** + * Prefix string for the namespace. + */ + public String prefix; + + /** + * Delimiter between names in this namespace. + */ + public char delimiter; + + /** + * Parse a namespace element out of the response. + * + * @param r the Response to parse + * @exception ProtocolException for any protocol errors + */ + public Namespace(Response r) throws ProtocolException { + // Namespace_Element = "(" string SP (<"> QUOTED_CHAR <"> / nil) + // *(Namespace_Response_Extension) ")" + if (!r.isNextNonSpace('(')) + throw new ProtocolException( + "Missing '(' at start of Namespace"); + // first, the prefix + prefix = r.readString(); + if (!r.supportsUtf8()) + prefix = BASE64MailboxDecoder.decode(prefix); + r.skipSpaces(); + // delimiter is a quoted character or NIL + if (r.peekByte() == '"') { + r.readByte(); + delimiter = (char)r.readByte(); + if (delimiter == '\\') + delimiter = (char)r.readByte(); + if (r.readByte() != '"') + throw new ProtocolException( + "Missing '\"' at end of QUOTED_CHAR"); + } else { + String s = r.readAtom(); + if (s == null) + throw new ProtocolException("Expected NIL, got null"); + if (!s.equalsIgnoreCase("NIL")) + throw new ProtocolException("Expected NIL, got " + s); + delimiter = 0; + } + // at end of Namespace data? + if (r.isNextNonSpace(')')) + return; + + // otherwise, must be a Namespace_Response_Extension + // Namespace_Response_Extension = SP string SP + // "(" string *(SP string) ")" + r.readString(); + r.skipSpaces(); + r.readStringList(); + if (!r.isNextNonSpace(')')) + throw new ProtocolException("Missing ')' at end of Namespace"); + } + }; + + /** + * The personal namespaces. + * May be null. + */ + public Namespace[] personal; + + /** + * The namespaces for other users. + * May be null. + */ + public Namespace[] otherUsers; + + /** + * The shared namespace. + * May be null. + */ + public Namespace[] shared; + + /** + * Parse out all the namespaces. + * + * @param r the Response to parse + * @throws ProtocolException for any protocol errors + */ + public Namespaces(Response r) throws ProtocolException { + personal = getNamespaces(r); + otherUsers = getNamespaces(r); + shared = getNamespaces(r); + } + + /** + * Parse out one of the three sets of namespaces. + */ + private Namespace[] getNamespaces(Response r) throws ProtocolException { + // Namespace = nil / "(" 1*( Namespace_Element) ")" + if (r.isNextNonSpace('(')) { + List v = new ArrayList<>(); + do { + Namespace ns = new Namespace(r); + v.add(ns); + } while (!r.isNextNonSpace(')')); + return v.toArray(new Namespace[v.size()]); + } else { + String s = r.readAtom(); + if (s == null) + throw new ProtocolException("Expected NIL, got null"); + if (!s.equalsIgnoreCase("NIL")) + throw new ProtocolException("Expected NIL, got " + s); + return null; + } + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/RFC822DATA.java b/app/src/main/java/com/sun/mail/imap/protocol/RFC822DATA.java new file mode 100644 index 0000000000..bda754f61d --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/RFC822DATA.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.io.ByteArrayInputStream; +import com.sun.mail.iap.*; +import com.sun.mail.util.ASCIIUtility; + +/** + * The RFC822 response data item. + * + * @author John Mani + * @author Bill Shannon + */ + +public class RFC822DATA implements Item { + + static final char[] name = {'R','F','C','8','2','2'}; + private final int msgno; + private final ByteArray data; + private final boolean isHeader; + + /** + * Constructor, header flag is false. + * + * @param r the FetchResponse + * @exception ParsingException for parsing failures + */ + public RFC822DATA(FetchResponse r) throws ParsingException { + this(r, false); + } + + /** + * Constructor, specifying header flag. + * + * @param r the FetchResponse + * @param isHeader just header information? + * @exception ParsingException for parsing failures + */ + public RFC822DATA(FetchResponse r, boolean isHeader) + throws ParsingException { + this.isHeader = isHeader; + msgno = r.getNumber(); + r.skipSpaces(); + data = r.readByteArray(); + } + + public ByteArray getByteArray() { + return data; + } + + public ByteArrayInputStream getByteArrayInputStream() { + if (data != null) + return data.toByteArrayInputStream(); + else + return null; + } + + public boolean isHeader() { + return isHeader; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/RFC822SIZE.java b/app/src/main/java/com/sun/mail/imap/protocol/RFC822SIZE.java new file mode 100644 index 0000000000..59118f1583 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/RFC822SIZE.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import com.sun.mail.iap.*; + +/** + * An RFC822SIZE FETCH item. + * + * @author John Mani + */ + +public class RFC822SIZE implements Item { + + static final char[] name = {'R','F','C','8','2','2','.','S','I','Z','E'}; + public int msgno; + + public long size; + + /** + * Constructor. + * + * @param r the FetchResponse + * @exception ParsingException for parsing failures + */ + public RFC822SIZE(FetchResponse r) throws ParsingException { + msgno = r.getNumber(); + r.skipSpaces(); + size = r.readLong(); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/SaslAuthenticator.java b/app/src/main/java/com/sun/mail/imap/protocol/SaslAuthenticator.java new file mode 100644 index 0000000000..4c31c76715 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/SaslAuthenticator.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import com.sun.mail.iap.ProtocolException; + +/** + * Interface to make it easier to call IMAPSaslAuthenticator. + */ + +public interface SaslAuthenticator { + public boolean authenticate(String[] mechs, String realm, String authzid, + String u, String p) throws ProtocolException; + +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/SearchSequence.java b/app/src/main/java/com/sun/mail/imap/protocol/SearchSequence.java new file mode 100644 index 0000000000..c1388373ca --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/SearchSequence.java @@ -0,0 +1,527 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.*; +import java.io.IOException; + +import javax.mail.*; +import javax.mail.search.*; +import com.sun.mail.iap.*; +import com.sun.mail.imap.OlderTerm; +import com.sun.mail.imap.YoungerTerm; +import com.sun.mail.imap.ModifiedSinceTerm; + +/** + * This class traverses a search-tree and generates the + * corresponding IMAP search sequence. + * + * Each IMAPProtocol instance contains an instance of this class, + * which might be subclassed by subclasses of IMAPProtocol to add + * support for additional product-specific search terms. + * + * @author John Mani + * @author Bill Shannon + */ +public class SearchSequence { + + private IMAPProtocol protocol; // for hasCapability checks; may be null + + /** + * Create a SearchSequence for this IMAPProtocol. + * + * @param p the IMAPProtocol object for the server + * @since JavaMail 1.6.0 + */ + public SearchSequence(IMAPProtocol p) { + protocol = p; + } + + /** + * Create a SearchSequence. + */ + @Deprecated + public SearchSequence() { + } + + /** + * Generate the IMAP search sequence for the given search expression. + * + * @param term the search term + * @param charset charset for the search + * @return the SEARCH Argument + * @exception SearchException for failures + * @exception IOException for I/O errors + */ + public Argument generateSequence(SearchTerm term, String charset) + throws SearchException, IOException { + /* + * Call the appropriate handler depending on the type of + * the search-term ... + */ + if (term instanceof AndTerm) // AND + return and((AndTerm)term, charset); + else if (term instanceof OrTerm) // OR + return or((OrTerm)term, charset); + else if (term instanceof NotTerm) // NOT + return not((NotTerm)term, charset); + else if (term instanceof HeaderTerm) // HEADER + return header((HeaderTerm)term, charset); + else if (term instanceof FlagTerm) // FLAG + return flag((FlagTerm)term); + else if (term instanceof FromTerm) { // FROM + FromTerm fterm = (FromTerm)term; + return from(fterm.getAddress().toString(), charset); + } + else if (term instanceof FromStringTerm) { // FROM + FromStringTerm fterm = (FromStringTerm)term; + return from(fterm.getPattern(), charset); + } + else if (term instanceof RecipientTerm) { // RECIPIENT + RecipientTerm rterm = (RecipientTerm)term; + return recipient(rterm.getRecipientType(), + rterm.getAddress().toString(), + charset); + } + else if (term instanceof RecipientStringTerm) { // RECIPIENT + RecipientStringTerm rterm = (RecipientStringTerm)term; + return recipient(rterm.getRecipientType(), + rterm.getPattern(), + charset); + } + else if (term instanceof SubjectTerm) // SUBJECT + return subject((SubjectTerm)term, charset); + else if (term instanceof BodyTerm) // BODY + return body((BodyTerm)term, charset); + else if (term instanceof SizeTerm) // SIZE + return size((SizeTerm)term); + else if (term instanceof SentDateTerm) // SENTDATE + return sentdate((SentDateTerm)term); + else if (term instanceof ReceivedDateTerm) // INTERNALDATE + return receiveddate((ReceivedDateTerm)term); + else if (term instanceof OlderTerm) // RFC 5032 OLDER + return older((OlderTerm)term); + else if (term instanceof YoungerTerm) // RFC 5032 YOUNGER + return younger((YoungerTerm)term); + else if (term instanceof MessageIDTerm) // MessageID + return messageid((MessageIDTerm)term, charset); + else if (term instanceof ModifiedSinceTerm) // RFC 4551 MODSEQ + return modifiedSince((ModifiedSinceTerm)term); + else + throw new SearchException("Search too complex"); + } + + /** + * Check if the "text" terms in the given SearchTerm contain + * non US-ASCII characters. + * + * @param term the search term + * @return true if only ASCII + */ + public static boolean isAscii(SearchTerm term) { + if (term instanceof AndTerm) + return isAscii(((AndTerm)term).getTerms()); + else if (term instanceof OrTerm) + return isAscii(((OrTerm)term).getTerms()); + else if (term instanceof NotTerm) + return isAscii(((NotTerm)term).getTerm()); + else if (term instanceof StringTerm) + return isAscii(((StringTerm)term).getPattern()); + else if (term instanceof AddressTerm) + return isAscii(((AddressTerm)term).getAddress().toString()); + + // Any other term returns true. + return true; + } + + /** + * Check if any of the "text" terms in the given SearchTerms contain + * non US-ASCII characters. + * + * @param terms the search terms + * @return true if only ASCII + */ + public static boolean isAscii(SearchTerm[] terms) { + for (int i = 0; i < terms.length; i++) + if (!isAscii(terms[i])) // outta here ! + return false; + return true; + } + + /** + * Does this string contain only ASCII characters? + * + * @param s the string + * @return true if only ASCII + */ + public static boolean isAscii(String s) { + int l = s.length(); + + for (int i=0; i < l; i++) { + if ((int)s.charAt(i) > 0177) // non-ascii + return false; + } + return true; + } + + protected Argument and(AndTerm term, String charset) + throws SearchException, IOException { + // Combine the sequences for both terms + SearchTerm[] terms = term.getTerms(); + // Generate the search sequence for the first term + Argument result = generateSequence(terms[0], charset); + // Append other terms + for (int i = 1; i < terms.length; i++) + result.append(generateSequence(terms[i], charset)); + return result; + } + + protected Argument or(OrTerm term, String charset) + throws SearchException, IOException { + SearchTerm[] terms = term.getTerms(); + + /* The IMAP OR operator takes only two operands. So if + * we have more than 2 operands, group them into 2-operand + * OR Terms. + */ + if (terms.length > 2) { + SearchTerm t = terms[0]; + + // Include rest of the terms + for (int i = 1; i < terms.length; i++) + t = new OrTerm(t, terms[i]); + + term = (OrTerm)t; // set 'term' to the new jumbo OrTerm we + // just created + terms = term.getTerms(); + } + + // 'term' now has only two operands + Argument result = new Argument(); + + // Add the OR search-key, if more than one term + if (terms.length > 1) + result.writeAtom("OR"); + + /* If this term is an AND expression, we need to enclose it + * within paranthesis. + * + * AND expressions are either AndTerms or FlagTerms + */ + if (terms[0] instanceof AndTerm || terms[0] instanceof FlagTerm) + result.writeArgument(generateSequence(terms[0], charset)); + else + result.append(generateSequence(terms[0], charset)); + + // Repeat the above for the second term, if there is one + if (terms.length > 1) { + if (terms[1] instanceof AndTerm || terms[1] instanceof FlagTerm) + result.writeArgument(generateSequence(terms[1], charset)); + else + result.append(generateSequence(terms[1], charset)); + } + + return result; + } + + protected Argument not(NotTerm term, String charset) + throws SearchException, IOException { + Argument result = new Argument(); + + // Add the NOT search-key + result.writeAtom("NOT"); + + /* If this term is an AND expression, we need to enclose it + * within paranthesis. + * + * AND expressions are either AndTerms or FlagTerms + */ + SearchTerm nterm = term.getTerm(); + if (nterm instanceof AndTerm || nterm instanceof FlagTerm) + result.writeArgument(generateSequence(nterm, charset)); + else + result.append(generateSequence(nterm, charset)); + + return result; + } + + protected Argument header(HeaderTerm term, String charset) + throws SearchException, IOException { + Argument result = new Argument(); + result.writeAtom("HEADER"); + result.writeString(term.getHeaderName()); + result.writeString(term.getPattern(), charset); + return result; + } + + protected Argument messageid(MessageIDTerm term, String charset) + throws SearchException, IOException { + Argument result = new Argument(); + result.writeAtom("HEADER"); + result.writeString("Message-ID"); + // XXX confirm that charset conversion ought to be done + result.writeString(term.getPattern(), charset); + return result; + } + + protected Argument flag(FlagTerm term) throws SearchException { + boolean set = term.getTestSet(); + + Argument result = new Argument(); + + Flags flags = term.getFlags(); + Flags.Flag[] sf = flags.getSystemFlags(); + String[] uf = flags.getUserFlags(); + if (sf.length == 0 && uf.length == 0) + throw new SearchException("Invalid FlagTerm"); + + for (int i = 0; i < sf.length; i++) { + if (sf[i] == Flags.Flag.DELETED) + result.writeAtom(set ? "DELETED": "UNDELETED"); + else if (sf[i] == Flags.Flag.ANSWERED) + result.writeAtom(set ? "ANSWERED": "UNANSWERED"); + else if (sf[i] == Flags.Flag.DRAFT) + result.writeAtom(set ? "DRAFT": "UNDRAFT"); + else if (sf[i] == Flags.Flag.FLAGGED) + result.writeAtom(set ? "FLAGGED": "UNFLAGGED"); + else if (sf[i] == Flags.Flag.RECENT) + result.writeAtom(set ? "RECENT": "OLD"); + else if (sf[i] == Flags.Flag.SEEN) + result.writeAtom(set ? "SEEN": "UNSEEN"); + } + + for (int i = 0; i < uf.length; i++) { + result.writeAtom(set ? "KEYWORD" : "UNKEYWORD"); + result.writeAtom(uf[i]); + } + + return result; + } + + protected Argument from(String address, String charset) + throws SearchException, IOException { + Argument result = new Argument(); + result.writeAtom("FROM"); + result.writeString(address, charset); + return result; + } + + protected Argument recipient(Message.RecipientType type, + String address, String charset) + throws SearchException, IOException { + Argument result = new Argument(); + + if (type == Message.RecipientType.TO) + result.writeAtom("TO"); + else if (type == Message.RecipientType.CC) + result.writeAtom("CC"); + else if (type == Message.RecipientType.BCC) + result.writeAtom("BCC"); + else + throw new SearchException("Illegal Recipient type"); + + result.writeString(address, charset); + return result; + } + + protected Argument subject(SubjectTerm term, String charset) + throws SearchException, IOException { + Argument result = new Argument(); + + result.writeAtom("SUBJECT"); + result.writeString(term.getPattern(), charset); + return result; + } + + protected Argument body(BodyTerm term, String charset) + throws SearchException, IOException { + Argument result = new Argument(); + + result.writeAtom("BODY"); + result.writeString(term.getPattern(), charset); + return result; + } + + protected Argument size(SizeTerm term) + throws SearchException { + Argument result = new Argument(); + + switch (term.getComparison()) { + case ComparisonTerm.GT: + result.writeAtom("LARGER"); + break; + case ComparisonTerm.LT: + result.writeAtom("SMALLER"); + break; + default: + // GT and LT is all we get from IMAP for size + throw new SearchException("Cannot handle Comparison"); + } + + result.writeNumber(term.getNumber()); + return result; + } + + // Date SEARCH stuff ... + + // NOTE: The built-in IMAP date comparisons are equivalent to + // "<" (BEFORE), "=" (ON), and ">=" (SINCE)!!! + // There is no built-in greater-than comparison! + + /** + * Print an IMAP Date string, that is suitable for the Date + * SEARCH commands. + * + * The IMAP Date string is : + * date ::= date_day "-" date_month "-" date_year + * + * Note that this format does not contain the TimeZone + */ + private static String monthTable[] = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" + }; + + // A GregorianCalendar object in the current timezone + protected Calendar cal = new GregorianCalendar(); + + protected String toIMAPDate(Date date) { + StringBuilder s = new StringBuilder(); + + cal.setTime(date); + + s.append(cal.get(Calendar.DATE)).append("-"); + s.append(monthTable[cal.get(Calendar.MONTH)]).append('-'); + s.append(cal.get(Calendar.YEAR)); + + return s.toString(); + } + + protected Argument sentdate(DateTerm term) + throws SearchException { + Argument result = new Argument(); + String date = toIMAPDate(term.getDate()); + + switch (term.getComparison()) { + case ComparisonTerm.GT: + result.writeAtom("NOT SENTON " + date + " SENTSINCE " + date); + break; + case ComparisonTerm.EQ: + result.writeAtom("SENTON " + date); + break; + case ComparisonTerm.LT: + result.writeAtom("SENTBEFORE " + date); + break; + case ComparisonTerm.GE: + result.writeAtom("SENTSINCE " + date); + break; + case ComparisonTerm.LE: + result.writeAtom("OR SENTBEFORE " + date + " SENTON " + date); + break; + case ComparisonTerm.NE: + result.writeAtom("NOT SENTON " + date); + break; + default: + throw new SearchException("Cannot handle Date Comparison"); + } + + return result; + } + + protected Argument receiveddate(DateTerm term) + throws SearchException { + Argument result = new Argument(); + String date = toIMAPDate(term.getDate()); + + switch (term.getComparison()) { + case ComparisonTerm.GT: + result.writeAtom("NOT ON " + date + " SINCE " + date); + break; + case ComparisonTerm.EQ: + result.writeAtom("ON " + date); + break; + case ComparisonTerm.LT: + result.writeAtom("BEFORE " + date); + break; + case ComparisonTerm.GE: + result.writeAtom("SINCE " + date); + break; + case ComparisonTerm.LE: + result.writeAtom("OR BEFORE " + date + " ON " + date); + break; + case ComparisonTerm.NE: + result.writeAtom("NOT ON " + date); + break; + default: + throw new SearchException("Cannot handle Date Comparison"); + } + + return result; + } + + /** + * Generate argument for OlderTerm. + * + * @param term the search term + * @return the SEARCH Argument + * @exception SearchException for failures + * @since JavaMail 1.5.1 + */ + protected Argument older(OlderTerm term) throws SearchException { + if (protocol != null && !protocol.hasCapability("WITHIN")) + throw new SearchException("Server doesn't support OLDER searches"); + Argument result = new Argument(); + result.writeAtom("OLDER"); + result.writeNumber(term.getInterval()); + return result; + } + + /** + * Generate argument for YoungerTerm. + * + * @param term the search term + * @return the SEARCH Argument + * @exception SearchException for failures + * @since JavaMail 1.5.1 + */ + protected Argument younger(YoungerTerm term) throws SearchException { + if (protocol != null && !protocol.hasCapability("WITHIN")) + throw new SearchException("Server doesn't support YOUNGER searches"); + Argument result = new Argument(); + result.writeAtom("YOUNGER"); + result.writeNumber(term.getInterval()); + return result; + } + + /** + * Generate argument for ModifiedSinceTerm. + * + * @param term the search term + * @return the SEARCH Argument + * @exception SearchException for failures + * @since JavaMail 1.5.1 + */ + protected Argument modifiedSince(ModifiedSinceTerm term) + throws SearchException { + if (protocol != null && !protocol.hasCapability("CONDSTORE")) + throw new SearchException("Server doesn't support MODSEQ searches"); + Argument result = new Argument(); + result.writeAtom("MODSEQ"); + result.writeNumber(term.getModSeq()); + return result; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/Status.java b/app/src/main/java/com/sun/mail/imap/protocol/Status.java new file mode 100644 index 0000000000..76d29c358b --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/Status.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.Map; +import java.util.HashMap; +import java.util.Locale; + +import com.sun.mail.iap.*; + +/** + * STATUS response. + * + * @author John Mani + * @author Bill Shannon + */ + +public class Status { + public String mbox = null; + public int total = -1; + public int recent = -1; + public long uidnext = -1; + public long uidvalidity = -1; + public int unseen = -1; + public long highestmodseq = -1; + public Map items; // any unknown items + + static final String[] standardItems = + { "MESSAGES", "RECENT", "UNSEEN", "UIDNEXT", "UIDVALIDITY" }; + + public Status(Response r) throws ParsingException { + // mailbox := astring + mbox = r.readAtomString(); + if (!r.supportsUtf8()) + mbox = BASE64MailboxDecoder.decode(mbox); + + // Workaround buggy IMAP servers that don't quote folder names + // with spaces. + final StringBuilder buffer = new StringBuilder(); + boolean onlySpaces = true; + + while (r.peekByte() != '(' && r.peekByte() != 0) { + final char next = (char)r.readByte(); + + buffer.append(next); + + if (next != ' ') { + onlySpaces = false; + } + } + + if (!onlySpaces) { + mbox = (mbox + buffer).trim(); + } + + if (r.readByte() != '(') + throw new ParsingException("parse error in STATUS"); + + do { + String attr = r.readAtom(); + if (attr == null) + throw new ParsingException("parse error in STATUS"); + if (attr.equalsIgnoreCase("MESSAGES")) + total = r.readNumber(); + else if (attr.equalsIgnoreCase("RECENT")) + recent = r.readNumber(); + else if (attr.equalsIgnoreCase("UIDNEXT")) + uidnext = r.readLong(); + else if (attr.equalsIgnoreCase("UIDVALIDITY")) + uidvalidity = r.readLong(); + else if (attr.equalsIgnoreCase("UNSEEN")) + unseen = r.readNumber(); + else if (attr.equalsIgnoreCase("HIGHESTMODSEQ")) + highestmodseq = r.readLong(); + else { + if (items == null) + items = new HashMap<>(); + items.put(attr.toUpperCase(Locale.ENGLISH), + Long.valueOf(r.readLong())); + } + } while (!r.isNextNonSpace(')')); + } + + /** + * Get the value for the STATUS item. + * + * @param item the STATUS item + * @return the value + * @since JavaMail 1.5.2 + */ + public long getItem(String item) { + item = item.toUpperCase(Locale.ENGLISH); + Long v; + long ret = -1; + if (items != null && (v = items.get(item)) != null) + ret = v.longValue(); + else if (item.equals("MESSAGES")) + ret = total; + else if (item.equals("RECENT")) + ret = recent; + else if (item.equals("UIDNEXT")) + ret = uidnext; + else if (item.equals("UIDVALIDITY")) + ret = uidvalidity; + else if (item.equals("UNSEEN")) + ret = unseen; + else if (item.equals("HIGHESTMODSEQ")) + ret = highestmodseq; + return ret; + } + + public static void add(Status s1, Status s2) { + if (s2.total != -1) + s1.total = s2.total; + if (s2.recent != -1) + s1.recent = s2.recent; + if (s2.uidnext != -1) + s1.uidnext = s2.uidnext; + if (s2.uidvalidity != -1) + s1.uidvalidity = s2.uidvalidity; + if (s2.unseen != -1) + s1.unseen = s2.unseen; + if (s2.highestmodseq != -1) + s1.highestmodseq = s2.highestmodseq; + if (s1.items == null) + s1.items = s2.items; + else if (s2.items != null) + s1.items.putAll(s2.items); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/UID.java b/app/src/main/java/com/sun/mail/imap/protocol/UID.java new file mode 100644 index 0000000000..58082c2104 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/UID.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import com.sun.mail.iap.*; + +/** + * This class represents the UID data item. + * + * @author John Mani + */ + +public class UID implements Item { + + static final char[] name = {'U','I','D'}; + public int seqnum; + + public long uid; + + /** + * Constructor. + * + * @param r the FetchResponse + * @exception ParsingException for parsing failures + */ + public UID(FetchResponse r) throws ParsingException { + seqnum = r.getNumber(); + r.skipSpaces(); + uid = r.readLong(); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/UIDSet.java b/app/src/main/java/com/sun/mail/imap/protocol/UIDSet.java new file mode 100644 index 0000000000..222858155c --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/UIDSet.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.List; +import java.util.ArrayList; +import java.util.StringTokenizer; + +/** + * This class holds the 'start' and 'end' for a range of UIDs. + * Just like MessageSet except using long instead of int. + */ +public class UIDSet { + + public long start; + public long end; + + public UIDSet() { } + + public UIDSet(long start, long end) { + this.start = start; + this.end = end; + } + + /** + * Count the total number of elements in a UIDSet + * + * @return the number of elements + */ + public long size() { + return end - start + 1; + } + + /** + * Convert an array of longs into an array of UIDSets + * + * @param uids the UIDs + * @return array of UIDSet objects + */ + public static UIDSet[] createUIDSets(long[] uids) { + if (uids == null) + return null; + List v = new ArrayList<>(); + int i,j; + + for (i=0; i < uids.length; i++) { + UIDSet ms = new UIDSet(); + ms.start = uids[i]; + + // Look for contiguous elements + for (j=i+1; j < uids.length; j++) { + if (uids[j] != uids[j-1] +1) + break; + } + ms.end = uids[j-1]; + v.add(ms); + i = j-1; // i gets incremented @ top of the loop + } + UIDSet[] uidset = new UIDSet[v.size()]; + return v.toArray(uidset); + } + + /** + * Parse a string in IMAP UID range format. + * + * @param uids UID string + * @return array of UIDSet objects + * @since JavaMail 1.5.1 + */ + public static UIDSet[] parseUIDSets(String uids) { + if (uids == null) + return null; + List v = new ArrayList<>(); + StringTokenizer st = new StringTokenizer(uids, ",:", true); + long start = -1; + UIDSet cur = null; + try { + while(st.hasMoreTokens()) { + String s = st.nextToken(); + if (s.equals(",")) { + if (cur != null) + v.add(cur); + cur = null; + } else if (s.equals(":")) { + // nothing to do, wait for next number + } else { // better be a number + long n = Long.parseLong(s); + if (cur != null) + cur.end = n; + else + cur = new UIDSet(n, n); + } + } + } catch (NumberFormatException nex) { + // give up and return what we have so far + } + if (cur != null) + v.add(cur); + UIDSet[] uidset = new UIDSet[v.size()]; + return v.toArray(uidset); + } + + /** + * Convert an array of UIDSets into an IMAP sequence range. + * + * @param uidset the UIDSets + * @return the IMAP sequence string + */ + public static String toString(UIDSet[] uidset) { + if (uidset == null) + return null; + if (uidset.length == 0) // Empty uidset + return ""; + + int i = 0; // uidset index + StringBuilder s = new StringBuilder(); + int size = uidset.length; + long start, end; + + for (;;) { + start = uidset[i].start; + end = uidset[i].end; + + if (end > start) + s.append(start).append(':').append(end); + else // end == start means only one element + s.append(start); + + i++; // Next UIDSet + if (i >= size) // No more UIDSets + break; + else + s.append(','); + } + return s.toString(); + } + + /** + * Convert an array of UIDSets into a array of long UIDs. + * + * @param uidset the UIDSets + * @return arrray of UIDs + * @since JavaMail 1.5.1 + */ + public static long[] toArray(UIDSet[] uidset) { + //return toArray(uidset, -1); + if (uidset == null) + return null; + long[] uids = new long[(int)UIDSet.size(uidset)]; + int i = 0; + for (UIDSet u : uidset) { + for (long n = u.start; n <= u.end; n++) + uids[i++] = n; + } + return uids; + } + + /** + * Convert an array of UIDSets into a array of long UIDs. + * Don't include any UIDs larger than uidmax. + * + * @param uidset the UIDSets + * @param uidmax maximum UID + * @return arrray of UIDs + * @since JavaMail 1.5.1 + */ + public static long[] toArray(UIDSet[] uidset, long uidmax) { + if (uidset == null) + return null; + long[] uids = new long[(int)UIDSet.size(uidset, uidmax)]; + int i = 0; + for (UIDSet u : uidset) { + for (long n = u.start; n <= u.end; n++) { + if (uidmax >= 0 && n > uidmax) + break; + uids[i++] = n; + } + } + return uids; + } + + /** + * Count the total number of elements in an array of UIDSets. + * + * @param uidset the UIDSets + * @return the number of elements + */ + public static long size(UIDSet[] uidset) { + long count = 0; + + if (uidset != null) + for (UIDSet u : uidset) + count += u.size(); + + return count; + } + + /** + * Count the total number of elements in an array of UIDSets. + * Don't count UIDs greater then uidmax. + * + * @since JavaMail 1.5.1 + */ + private static long size(UIDSet[] uidset, long uidmax) { + long count = 0; + + if (uidset != null) + for (UIDSet u : uidset) { + if (uidmax < 0) + count += u.size(); + else if (u.start <= uidmax) { + if (u.end < uidmax) + count += u.end - u.start + 1; + else + count += uidmax - u.start + 1; + } + } + + return count; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/package.html b/app/src/main/java/com/sun/mail/imap/protocol/package.html new file mode 100644 index 0000000000..370726e0fa --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/package.html @@ -0,0 +1,33 @@ + + + + + + +com.sun.mail.imap.protocol package + + + +

+This package includes internal IMAP support classes and +SHOULD NOT BE USED DIRECTLY BY APPLICATIONS. +

+ + + diff --git a/app/src/main/java/com/sun/mail/pop3/AppendStream.java b/app/src/main/java/com/sun/mail/pop3/AppendStream.java new file mode 100644 index 0000000000..cc56ce3569 --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/AppendStream.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; + +/** + * A stream for writing to the temp file, and when done can return a stream for + * reading the data just written. NOTE: We assume that only one thread is + * writing to the file at a time. + */ +class AppendStream extends OutputStream { + + private final WritableSharedFile tf; + private RandomAccessFile raf; + private final long start; + private long end; + + public AppendStream(WritableSharedFile tf) throws IOException { + this.tf = tf; + raf = tf.getWritableFile(); + start = raf.length(); + raf.seek(start); + } + + @Override + public void write(int b) throws IOException { + raf.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + raf.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + raf.write(b, off, len); + } + + @Override + public synchronized void close() throws IOException { + end = tf.updateLength(); + raf = null; // no more writing allowed + } + + public synchronized InputStream getInputStream() throws IOException { + return tf.newStream(start, end); + } +} diff --git a/app/src/main/java/com/sun/mail/pop3/DefaultFolder.java b/app/src/main/java/com/sun/mail/pop3/DefaultFolder.java new file mode 100644 index 0000000000..eaf9d6391c --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/DefaultFolder.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import javax.mail.*; + +/** + * The POP3 DefaultFolder. Only contains the "INBOX" folder. + * + * @author Christopher Cotton + */ +public class DefaultFolder extends Folder { + + DefaultFolder(POP3Store store) { + super(store); + } + + @Override + public String getName() { + return ""; + } + + @Override + public String getFullName() { + return ""; + } + + @Override + public Folder getParent() { + return null; + } + + @Override + public boolean exists() { + return true; + } + + @Override + public Folder[] list(String pattern) throws MessagingException { + Folder[] f = { getInbox() }; + return f; + } + + @Override + public char getSeparator() { + return '/'; + } + + @Override + public int getType() { + return HOLDS_FOLDERS; + } + + @Override + public boolean create(int type) throws MessagingException { + return false; + } + + @Override + public boolean hasNewMessages() throws MessagingException { + return false; + } + + @Override + public Folder getFolder(String name) throws MessagingException { + if (!name.equalsIgnoreCase("INBOX")) { + throw new MessagingException("only INBOX supported"); + } else { + return getInbox(); + } + } + + protected Folder getInbox() throws MessagingException { + return getStore().getFolder("INBOX"); + } + + + @Override + public boolean delete(boolean recurse) throws MessagingException { + throw new MethodNotSupportedException("delete"); + } + + @Override + public boolean renameTo(Folder f) throws MessagingException { + throw new MethodNotSupportedException("renameTo"); + } + + @Override + public void open(int mode) throws MessagingException { + throw new MethodNotSupportedException("open"); + } + + @Override + public void close(boolean expunge) throws MessagingException { + throw new MethodNotSupportedException("close"); + } + + @Override + public boolean isOpen() { + return false; + } + + @Override + public Flags getPermanentFlags() { + return new Flags(); // empty flags object + } + + @Override + public int getMessageCount() throws MessagingException { + return 0; + } + + @Override + public Message getMessage(int msgno) throws MessagingException { + throw new MethodNotSupportedException("getMessage"); + } + + @Override + public void appendMessages(Message[] msgs) throws MessagingException { + throw new MethodNotSupportedException("Append not supported"); + } + + @Override + public Message[] expunge() throws MessagingException { + throw new MethodNotSupportedException("expunge"); + } +} diff --git a/app/src/main/java/com/sun/mail/pop3/POP3Folder.java b/app/src/main/java/com/sun/mail/pop3/POP3Folder.java new file mode 100644 index 0000000000..a045a24ae2 --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/POP3Folder.java @@ -0,0 +1,611 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import javax.mail.*; +import javax.mail.event.*; +import java.io.InputStream; +import java.io.IOException; +import java.io.EOFException; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.lang.reflect.Constructor; + +import com.sun.mail.util.LineInputStream; +import com.sun.mail.util.MailLogger; +import java.util.ArrayList; +import java.util.List; + +/** + * A POP3 Folder (can only be "INBOX"). + * + * See the com.sun.mail.pop3 package + * documentation for further information on the POP3 protocol provider.

+ * + * @author Bill Shannon + * @author John Mani (ported to the javax.mail APIs) + */ +public class POP3Folder extends Folder { + + private String name; + private POP3Store store; + private volatile Protocol port; + private int total; + private int size; + private boolean exists = false; + private volatile boolean opened = false; + private POP3Message[] message_cache; + private boolean doneUidl = false; + private volatile TempFile fileCache = null; + private boolean forceClose; + + MailLogger logger; // package private, for POP3Message + + protected POP3Folder(POP3Store store, String name) { + super(store); + this.name = name; + this.store = store; + if (name.equalsIgnoreCase("INBOX")) + exists = true; + logger = new MailLogger(this.getClass(), "DEBUG POP3", + store.getSession().getDebug(), store.getSession().getDebugOut()); + } + + @Override + public String getName() { + return name; + } + + @Override + public String getFullName() { + return name; + } + + @Override + public Folder getParent() { + return new DefaultFolder(store); + } + + /** + * Always true for the folder "INBOX", always false for + * any other name. + * + * @return true for INBOX, false otherwise + */ + @Override + public boolean exists() { + return exists; + } + + /** + * Always throws MessagingException because no POP3 folders + * can contain subfolders. + * + * @exception MessagingException always + */ + @Override + public Folder[] list(String pattern) throws MessagingException { + throw new MessagingException("not a directory"); + } + + /** + * Always returns a NUL character because POP3 doesn't support a hierarchy. + * + * @return NUL + */ + @Override + public char getSeparator() { + return '\0'; + } + + /** + * Always returns Folder.HOLDS_MESSAGES. + * + * @return Folder.HOLDS_MESSAGES + */ + @Override + public int getType() { + return HOLDS_MESSAGES; + } + + /** + * Always returns false; the POP3 protocol doesn't + * support creating folders. + * + * @return false + */ + @Override + public boolean create(int type) throws MessagingException { + return false; + } + + /** + * Always returns false; the POP3 protocol provides + * no way to determine when a new message arrives. + * + * @return false + */ + @Override + public boolean hasNewMessages() throws MessagingException { + return false; // no way to know + } + + /** + * Always throws MessagingException because no POP3 folders + * can contain subfolders. + * + * @exception MessagingException always + */ + @Override + public Folder getFolder(String name) throws MessagingException { + throw new MessagingException("not a directory"); + } + + /** + * Always throws MethodNotSupportedException + * because the POP3 protocol doesn't allow the INBOX to + * be deleted. + * + * @exception MethodNotSupportedException always + */ + @Override + public boolean delete(boolean recurse) throws MessagingException { + throw new MethodNotSupportedException("delete"); + } + + /** + * Always throws MethodNotSupportedException + * because the POP3 protocol doesn't support multiple folders. + * + * @exception MethodNotSupportedException always + */ + @Override + public boolean renameTo(Folder f) throws MessagingException { + throw new MethodNotSupportedException("renameTo"); + } + + /** + * Throws FolderNotFoundException unless this + * folder is named "INBOX". + * + * @exception FolderNotFoundException if not INBOX + * @exception AuthenticationFailedException authentication failures + * @exception MessagingException other open failures + */ + @Override + public synchronized void open(int mode) throws MessagingException { + checkClosed(); + if (!exists) + throw new FolderNotFoundException(this, "folder is not INBOX"); + + try { + port = store.getPort(this); + Status s = port.stat(); + total = s.total; + size = s.size; + this.mode = mode; + if (store.useFileCache) { + try { + fileCache = new TempFile(store.fileCacheDir); + } catch (IOException ex) { + logger.log(Level.FINE, "failed to create file cache", ex); + throw ex; // caught below + } + } + opened = true; + } catch (IOException ioex) { + try { + if (port != null) + port.quit(); + } catch (IOException ioex2) { + // ignore + } finally { + port = null; + store.closePort(this); + } + throw new MessagingException("Open failed", ioex); + } + + // Create the message cache array of appropriate size + message_cache = new POP3Message[total]; + doneUidl = false; + + notifyConnectionListeners(ConnectionEvent.OPENED); + } + + @Override + public synchronized void close(boolean expunge) throws MessagingException { + checkOpen(); + + try { + /* + * Some POP3 servers will mark messages for deletion when + * they're read. To prevent such messages from being + * deleted before the client deletes them, you can set + * the mail.pop3.rsetbeforequit property to true. This + * causes us to issue a POP3 RSET command to clear all + * the "marked for deletion" flags. We can then explicitly + * delete messages as desired. + */ + if (store.rsetBeforeQuit && !forceClose) + port.rset(); + POP3Message m; + if (expunge && mode == READ_WRITE && !forceClose) { + // find all messages marked deleted and issue DELE commands + for (int i = 0; i < message_cache.length; i++) { + if ((m = message_cache[i]) != null) { + if (m.isSet(Flags.Flag.DELETED)) + try { + port.dele(i + 1); + } catch (IOException ioex) { + throw new MessagingException( + "Exception deleting messages during close", + ioex); + } + } + } + } + + /* + * Flush and free all cached data for the messages. + */ + for (int i = 0; i < message_cache.length; i++) { + if ((m = message_cache[i]) != null) + m.invalidate(true); + } + + if (forceClose) + port.close(); + else + port.quit(); + } catch (IOException ex) { + // do nothing + } finally { + port = null; + store.closePort(this); + message_cache = null; + opened = false; + notifyConnectionListeners(ConnectionEvent.CLOSED); + if (fileCache != null) { + fileCache.close(); + fileCache = null; + } + } + } + + @Override + public synchronized boolean isOpen() { + if (!opened) + return false; + try { + if (!port.noop()) + throw new IOException("NOOP failed"); + } catch (IOException ioex) { + try { + close(false); + } catch (MessagingException mex) { + // ignore it + } + return false; + } + return true; + } + + /** + * Always returns an empty Flags object because + * the POP3 protocol doesn't support any permanent flags. + * + * @return empty Flags object + */ + @Override + public Flags getPermanentFlags() { + return new Flags(); // empty flags object + } + + /** + * Will not change while the folder is open because the POP3 + * protocol doesn't support notification of new messages + * arriving in open folders. + */ + @Override + public synchronized int getMessageCount() throws MessagingException { + if (!opened) + return -1; + checkReadable(); + return total; + } + + @Override + public synchronized Message getMessage(int msgno) + throws MessagingException { + checkOpen(); + + POP3Message m; + + // Assuming that msgno is <= total + if ((m = message_cache[msgno-1]) == null) { + m = createMessage(this, msgno); + message_cache[msgno-1] = m; + } + return m; + } + + protected POP3Message createMessage(Folder f, int msgno) + throws MessagingException { + POP3Message m = null; + Constructor cons = store.messageConstructor; + if (cons != null) { + try { + Object[] o = { this, Integer.valueOf(msgno) }; + m = (POP3Message)cons.newInstance(o); + } catch (Exception ex) { + // ignore + } + } + if (m == null) + m = new POP3Message(this, msgno); + return m; + } + + /** + * Always throws MethodNotSupportedException + * because the POP3 protocol doesn't support appending messages. + * + * @exception MethodNotSupportedException always + */ + @Override + public void appendMessages(Message[] msgs) throws MessagingException { + throw new MethodNotSupportedException("Append not supported"); + } + + /** + * Always throws MethodNotSupportedException + * because the POP3 protocol doesn't support expunging messages + * without closing the folder; call the {@link #close close} method + * with the expunge argument set to true + * instead. + * + * @exception MethodNotSupportedException always + */ + @Override + public Message[] expunge() throws MessagingException { + throw new MethodNotSupportedException("Expunge not supported"); + } + + /** + * Prefetch information about POP3 messages. + * If the FetchProfile contains UIDFolder.FetchProfileItem.UID, + * POP3 UIDs for all messages in the folder are fetched using the POP3 + * UIDL command. + * If the FetchProfile contains FetchProfile.Item.ENVELOPE, + * the headers and size of all messages are fetched using the POP3 TOP + * and LIST commands. + */ + @Override + public synchronized void fetch(Message[] msgs, FetchProfile fp) + throws MessagingException { + checkReadable(); + if (!doneUidl && store.supportsUidl && + fp.contains(UIDFolder.FetchProfileItem.UID)) { + /* + * Since the POP3 protocol only lets us fetch the UID + * for a single message or for all messages, we go ahead + * and fetch UIDs for all messages here, ignoring the msgs + * parameter. We could be more intelligent and base this + * decision on the number of messages fetched, or the + * percentage of the total number of messages fetched. + */ + String[] uids = new String[message_cache.length]; + try { + if (!port.uidl(uids)) + return; + } catch (EOFException eex) { + close(false); + throw new FolderClosedException(this, eex.toString()); + } catch (IOException ex) { + throw new MessagingException("error getting UIDL", ex); + } + for (int i = 0; i < uids.length; i++) { + if (uids[i] == null) + continue; + POP3Message m = (POP3Message)getMessage(i + 1); + m.uid = uids[i]; + } + doneUidl = true; // only do this once + } + if (fp.contains(FetchProfile.Item.ENVELOPE)) { + for (int i = 0; i < msgs.length; i++) { + try { + POP3Message msg = (POP3Message)msgs[i]; + // fetch headers + msg.getHeader(""); + // fetch message size + msg.getSize(); + } catch (MessageRemovedException mex) { + // should never happen, but ignore it if it does + } + } + } + } + + /** + * Return the unique ID string for this message, or null if + * not available. Uses the POP3 UIDL command. + * + * @param msg the message + * @return unique ID string + * @exception MessagingException for failures + */ + public synchronized String getUID(Message msg) throws MessagingException { + checkOpen(); + if (!(msg instanceof POP3Message)) + throw new MessagingException("message is not a POP3Message"); + POP3Message m = (POP3Message)msg; + try { + if (!store.supportsUidl) + return null; + if (m.uid == POP3Message.UNKNOWN) + m.uid = port.uidl(m.getMessageNumber()); + return m.uid; + } catch (EOFException eex) { + close(false); + throw new FolderClosedException(this, eex.toString()); + } catch (IOException ex) { + throw new MessagingException("error getting UIDL", ex); + } + } + + /** + * Return the size of this folder, as was returned by the POP3 STAT + * command when this folder was opened. + * + * @return folder size + * @exception IllegalStateException if the folder isn't open + * @exception MessagingException for other failures + */ + public synchronized int getSize() throws MessagingException { + checkOpen(); + return size; + } + + /** + * Return the sizes of all messages in this folder, as returned + * by the POP3 LIST command. Each entry in the array corresponds + * to a message; entry i corresponds to message number i+1. + * + * @return array of message sizes + * @exception IllegalStateException if the folder isn't open + * @exception MessagingException for other failures + * @since JavaMail 1.3.3 + */ + public synchronized int[] getSizes() throws MessagingException { + checkOpen(); + int sizes[] = new int[total]; + InputStream is = null; + LineInputStream lis = null; + try { + is = port.list(); + lis = new LineInputStream(is); + String line; + while ((line = lis.readLine()) != null) { + try { + StringTokenizer st = new StringTokenizer(line); + int msgnum = Integer.parseInt(st.nextToken()); + int size = Integer.parseInt(st.nextToken()); + if (msgnum > 0 && msgnum <= total) + sizes[msgnum - 1] = size; + } catch (RuntimeException e) { + } + } + } catch (IOException ex) { + // ignore it? + } finally { + try { + if (lis != null) + lis.close(); + } catch (IOException cex) { } + try { + if (is != null) + is.close(); + } catch (IOException cex) { } + } + return sizes; + } + + /** + * Return the raw results of the POP3 LIST command with no arguments. + * + * @return InputStream containing results + * @exception IllegalStateException if the folder isn't open + * @exception IOException for I/O errors talking to the server + * @exception MessagingException for other errors + * @since JavaMail 1.3.3 + */ + public synchronized InputStream listCommand() + throws MessagingException, IOException { + checkOpen(); + return port.list(); + } + + /** + * Close the folder when we're finalized. + */ + @Override + protected void finalize() throws Throwable { + forceClose = !store.finalizeCleanClose; + try { + if (opened) + close(false); + } finally { + super.finalize(); + forceClose = false; + } + } + + /* Ensure the folder is open */ + private void checkOpen() throws IllegalStateException { + if (!opened) + throw new IllegalStateException("Folder is not Open"); + } + + /* Ensure the folder is not open */ + private void checkClosed() throws IllegalStateException { + if (opened) + throw new IllegalStateException("Folder is Open"); + } + + /* Ensure the folder is open & readable */ + private void checkReadable() throws IllegalStateException { + if (!opened || (mode != READ_ONLY && mode != READ_WRITE)) + throw new IllegalStateException("Folder is not Readable"); + } + + /* Ensure the folder is open & writable */ + /* + private void checkWritable() throws IllegalStateException { + if (!opened || mode != READ_WRITE) + throw new IllegalStateException("Folder is not Writable"); + } + */ + + /** + * Centralize access to the Protocol object by POP3Message + * objects so that they will fail appropriately when the folder + * is closed. + */ + Protocol getProtocol() throws MessagingException { + Protocol p = port; // read it before close() can set it to null + checkOpen(); + // close() might happen here + return p; + } + + /* + * Only here to make accessible to POP3Message. + */ + @Override + protected void notifyMessageChangedListeners(int type, Message m) { + super.notifyMessageChangedListeners(type, m); + } + + /** + * Used by POP3Message. + */ + TempFile getFileCache() { + return fileCache; + } +} diff --git a/app/src/main/java/com/sun/mail/pop3/POP3Message.java b/app/src/main/java/com/sun/mail/pop3/POP3Message.java new file mode 100644 index 0000000000..7bc9c9fabe --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/POP3Message.java @@ -0,0 +1,644 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.io.*; +import java.util.Enumeration; +import java.util.logging.Level; +import java.lang.ref.SoftReference; +import javax.mail.*; +import javax.mail.internet.*; +import javax.mail.event.*; +import com.sun.mail.util.ReadableMime; + +/** + * A POP3 Message. Just like a MimeMessage except that + * some things are not supported. + * + * @author Bill Shannon + */ +public class POP3Message extends MimeMessage implements ReadableMime { + + /* + * Our locking strategy is to always lock the POP3Folder before the + * POP3Message so we have to be careful to drop our lock before calling + * back to the folder to close it and notify of connection lost events. + */ + + // flag to indicate we haven't tried to fetch the UID yet + static final String UNKNOWN = "UNKNOWN"; + + private POP3Folder folder; // overrides folder in MimeMessage + private int hdrSize = -1; + private int msgSize = -1; + String uid = UNKNOWN; // controlled by folder lock + + // rawData itself is never null + private SoftReference rawData + = new SoftReference<>(null); + + public POP3Message(Folder folder, int msgno) + throws MessagingException { + super(folder, msgno); + assert folder instanceof POP3Folder; + this.folder = (POP3Folder)folder; + } + + /** + * Set the specified flags on this message to the specified value. + * + * @param newFlags the flags to be set + * @param set the value to be set + */ + @Override + public synchronized void setFlags(Flags newFlags, boolean set) + throws MessagingException { + Flags oldFlags = (Flags)flags.clone(); + super.setFlags(newFlags, set); + if (!flags.equals(oldFlags)) + folder.notifyMessageChangedListeners( + MessageChangedEvent.FLAGS_CHANGED, this); + } + + /** + * Return the size of the content of this message in bytes. + * Returns -1 if the size cannot be determined.

+ * + * Note that this number may not be an exact measure of the + * content size and may or may not account for any transfer + * encoding of the content.

+ * + * @return size of content in bytes + * @exception MessagingException for failures + */ + @Override + public int getSize() throws MessagingException { + try { + synchronized (this) { + // if we already have the size, return it + if (msgSize > 0) + return msgSize; + } + + /* + * Use LIST to determine the entire message + * size and subtract out the header size + * (which may involve loading the headers, + * which may load the content as a side effect). + * If the content is loaded as a side effect of + * loading the headers, it will set the size. + * + * Make sure to call loadHeaders() outside of the + * synchronization block. There's a potential race + * condition here but synchronization will occur in + * loadHeaders() to make sure the headers are only + * loaded once, and again in the following block to + * only compute msgSize once. + */ + if (headers == null) + loadHeaders(); + + synchronized (this) { + if (msgSize < 0) + msgSize = folder.getProtocol().list(msgnum) - hdrSize; + return msgSize; + } + } catch (EOFException eex) { + folder.close(false); + throw new FolderClosedException(folder, eex.toString()); + } catch (IOException ex) { + throw new MessagingException("error getting size", ex); + } + } + + /** + * Produce the raw bytes of the message. The data is fetched using + * the POP3 RETR command. If skipHeader is true, just the content + * is returned. + */ + private InputStream getRawStream(boolean skipHeader) + throws MessagingException { + InputStream rawcontent = null; + try { + synchronized(this) { + rawcontent = rawData.get(); + if (rawcontent == null) { + TempFile cache = folder.getFileCache(); + if (cache != null) { + if (folder.logger.isLoggable(Level.FINE)) + folder.logger.fine("caching message #" + msgnum + + " in temp file"); + AppendStream os = cache.getAppendStream(); + BufferedOutputStream bos = new BufferedOutputStream(os); + try { + folder.getProtocol().retr(msgnum, bos); + } finally { + bos.close(); + } + rawcontent = os.getInputStream(); + } else { + rawcontent = folder.getProtocol().retr(msgnum, + msgSize > 0 ? msgSize + hdrSize : 0); + } + if (rawcontent == null) { + expunged = true; + throw new MessageRemovedException( + "can't retrieve message #" + msgnum + + " in POP3Message.getContentStream"); // XXX - what else? + } + + if (headers == null || + ((POP3Store)(folder.getStore())).forgetTopHeaders) { + headers = new InternetHeaders(rawcontent); + hdrSize = + (int)((SharedInputStream)rawcontent).getPosition(); + } else { + /* + * Already have the headers, have to skip the headers + * in the content array and return the body. + * + * XXX - It seems that some mail servers return slightly + * different headers in the RETR results than were returned + * in the TOP results, so we can't depend on remembering + * the size of the headers from the TOP command and just + * skipping that many bytes. Instead, we have to process + * the content, skipping over the header until we come to + * the empty line that separates the header from the body. + */ + int offset = 0; + for (;;) { + int len = 0; // number of bytes in this line + int c1; + while ((c1 = rawcontent.read()) >= 0) { + if (c1 == '\n') // end of line + break; + else if (c1 == '\r') { + // got CR, is the next char LF? + if (rawcontent.available() > 0) { + rawcontent.mark(1); + if (rawcontent.read() != '\n') + rawcontent.reset(); + } + break; // in any case, end of line + } + + // not CR, NL, or CRLF, count the byte + len++; + } + // here when end of line or out of data + + // if out of data, we're done + if (rawcontent.available() == 0) + break; + + // if it was an empty line, we're done + if (len == 0) + break; + } + hdrSize = + (int)((SharedInputStream)rawcontent).getPosition(); + } + + // skipped the header, the message is what's left + msgSize = rawcontent.available(); + + rawData = new SoftReference<>(rawcontent); + } + } + } catch (EOFException eex) { + folder.close(false); + throw new FolderClosedException(folder, eex.toString()); + } catch (IOException ex) { + throw new MessagingException("error fetching POP3 content", ex); + } + + /* + * We have a cached stream, but we need to return + * a fresh stream to read from the beginning and + * that can be safely closed. + */ + rawcontent = ((SharedInputStream)rawcontent).newStream( + skipHeader ? hdrSize : 0, -1); + return rawcontent; + } + + /** + * Produce the raw bytes of the content. The data is fetched using + * the POP3 RETR command. + * + * @see #contentStream + */ + @Override + protected synchronized InputStream getContentStream() + throws MessagingException { + if (contentStream != null) + return ((SharedInputStream)contentStream).newStream(0, -1); + + InputStream cstream = getRawStream(true); + + /* + * Keep a hard reference to the data if we're using a file + * cache or if the "mail.pop3.keepmessagecontent" prop is set. + */ + TempFile cache = folder.getFileCache(); + if (cache != null || + ((POP3Store)(folder.getStore())).keepMessageContent) + contentStream = ((SharedInputStream)cstream).newStream(0, -1); + return cstream; + } + + /** + * Return the MIME format stream corresponding to this message part. + * + * @return the MIME format stream + * @since JavaMail 1.4.5 + */ + @Override + public InputStream getMimeStream() throws MessagingException { + return getRawStream(false); + } + + /** + * Invalidate the cache of content for this message object, causing + * it to be fetched again from the server the next time it is needed. + * If invalidateHeaders is true, invalidate the headers + * as well. + * + * @param invalidateHeaders invalidate the headers as well? + */ + public synchronized void invalidate(boolean invalidateHeaders) { + content = null; + InputStream rstream = rawData.get(); + if (rstream != null) { + // note that if the content is in the file cache, it will be lost + // and fetched from the server if it's needed again + try { + rstream.close(); + } catch (IOException ex) { + // ignore it + } + rawData = new SoftReference<>(null); + } + if (contentStream != null) { + try { + contentStream.close(); + } catch (IOException ex) { + // ignore it + } + contentStream = null; + } + msgSize = -1; + if (invalidateHeaders) { + headers = null; + hdrSize = -1; + } + } + + /** + * Fetch the header of the message and the first n lines + * of the raw content of the message. The headers and data are + * available in the returned InputStream. + * + * @param n number of lines of content to fetch + * @return InputStream containing the message headers and n content lines + * @exception MessagingException for failures + */ + public InputStream top(int n) throws MessagingException { + try { + synchronized (this) { + return folder.getProtocol().top(msgnum, n); + } + } catch (EOFException eex) { + folder.close(false); + throw new FolderClosedException(folder, eex.toString()); + } catch (IOException ex) { + throw new MessagingException("error getting size", ex); + } + } + + /** + * Get all the headers for this header_name. Note that certain + * headers may be encoded as per RFC 2047 if they contain + * non US-ASCII characters and these should be decoded.

+ * + * @param name name of header + * @return array of headers + * @exception MessagingException for failures + * @see javax.mail.internet.MimeUtility + */ + @Override + public String[] getHeader(String name) + throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getHeader(name); + } + + /** + * Get all the headers for this header name, returned as a single + * String, with headers separated by the delimiter. If the + * delimiter is null, only the first header is + * returned. + * + * @param name the name of this header + * @param delimiter delimiter between returned headers + * @return the value fields for all headers with + * this name + * @exception MessagingException for failures + */ + @Override + public String getHeader(String name, String delimiter) + throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getHeader(name, delimiter); + } + + /** + * Set the value for this header_name. Throws IllegalWriteException + * because POP3 messages are read-only. + * + * @param name header name + * @param value header value + * @see javax.mail.internet.MimeUtility + * @exception IllegalWriteException because the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setHeader(String name, String value) + throws MessagingException { + // XXX - should check for read-only folder? + throw new IllegalWriteException("POP3 messages are read-only"); + } + + /** + * Add this value to the existing values for this header_name. + * Throws IllegalWriteException because POP3 messages are read-only. + * + * @param name header name + * @param value header value + * @see javax.mail.internet.MimeUtility + * @exception IllegalWriteException because the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + */ + @Override + public void addHeader(String name, String value) + throws MessagingException { + // XXX - should check for read-only folder? + throw new IllegalWriteException("POP3 messages are read-only"); + } + + /** + * Remove all headers with this name. + * Throws IllegalWriteException because POP3 messages are read-only. + * + * @exception IllegalWriteException because the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + */ + @Override + public void removeHeader(String name) + throws MessagingException { + // XXX - should check for read-only folder? + throw new IllegalWriteException("POP3 messages are read-only"); + } + + /** + * Return all the headers from this Message as an enumeration + * of Header objects.

+ * + * Note that certain headers may be encoded as per RFC 2047 + * if they contain non US-ASCII characters and these should + * be decoded.

+ * + * @return array of header objects + * @exception MessagingException for failures + * @see javax.mail.internet.MimeUtility + */ + @Override + public Enumeration

getAllHeaders() throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getAllHeaders(); + } + + /** + * Return matching headers from this Message as an Enumeration of + * Header objects. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration
getMatchingHeaders(String[] names) + throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getMatchingHeaders(names); + } + + /** + * Return non-matching headers from this Message as an + * Enumeration of Header objects. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration
getNonMatchingHeaders(String[] names) + throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getNonMatchingHeaders(names); + } + + /** + * Add a raw RFC822 header-line. + * Throws IllegalWriteException because POP3 messages are read-only. + * + * @exception IllegalWriteException because the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + */ + @Override + public void addHeaderLine(String line) throws MessagingException { + // XXX - should check for read-only folder? + throw new IllegalWriteException("POP3 messages are read-only"); + } + + /** + * Get all header lines as an Enumeration of Strings. A Header + * line is a raw RFC822 header-line, containing both the "name" + * and "value" field. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration getAllHeaderLines() throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getAllHeaderLines(); + } + + /** + * Get matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC822 header-line, containing both + * the "name" and "value" field. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration getMatchingHeaderLines(String[] names) + throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getMatchingHeaderLines(names); + } + + /** + * Get non-matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC822 header-line, containing both + * the "name" and "value" field. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration getNonMatchingHeaderLines(String[] names) + throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getNonMatchingHeaderLines(names); + } + + /** + * POP3 message can't be changed. This method throws + * IllegalWriteException. + * + * @exception IllegalWriteException because the underlying + * implementation does not support modification + */ + @Override + public void saveChanges() throws MessagingException { + // POP3 Messages are read-only + throw new IllegalWriteException("POP3 messages are read-only"); + } + + /** + * Output the message as an RFC 822 format stream, without + * specified headers. If the property "mail.pop3.cachewriteto" + * is set to "true", and ignoreList is null, and the message hasn't + * already been cached as a side effect of other operations, the message + * content is cached before being written. Otherwise, the message is + * streamed directly to the output stream without being cached. + * + * @exception IOException if an error occurs writing to the stream + * or if an error is generated by the + * javax.activation layer. + * @exception MessagingException for other failures + * @see javax.activation.DataHandler#writeTo + */ + @Override + public synchronized void writeTo(OutputStream os, String[] ignoreList) + throws IOException, MessagingException { + InputStream rawcontent = rawData.get(); + if (rawcontent == null && ignoreList == null && + !((POP3Store)(folder.getStore())).cacheWriteTo) { + if (folder.logger.isLoggable(Level.FINE)) + folder.logger.fine("streaming msg " + msgnum); + if (!folder.getProtocol().retr(msgnum, os)) { + expunged = true; + throw new MessageRemovedException("can't retrieve message #" + + msgnum + " in POP3Message.writeTo"); // XXX - what else? + } + } else if (rawcontent != null && ignoreList == null) { + // can just copy the cached data + InputStream in = ((SharedInputStream)rawcontent).newStream(0, -1); + try { + byte[] buf = new byte[16*1024]; + int len; + while ((len = in.read(buf)) > 0) + os.write(buf, 0, len); + } finally { + try { + if (in != null) + in.close(); + } catch (IOException ex) { } + } + } else + super.writeTo(os, ignoreList); + } + + /** + * Load the headers for this message into the InternetHeaders object. + * The headers are fetched using the POP3 TOP command. + */ + private void loadHeaders() throws MessagingException { + assert !Thread.holdsLock(this); + try { + boolean fetchContent = false; + synchronized (this) { + if (headers != null) // check again under lock + return; + InputStream hdrs = null; + if (((POP3Store)(folder.getStore())).disableTop || + (hdrs = folder.getProtocol().top(msgnum, 0)) == null) { + // possibly because the TOP command isn't supported, + // load headers as a side effect of loading the entire + // content. + fetchContent = true; + } else { + try { + hdrSize = hdrs.available(); + headers = new InternetHeaders(hdrs); + } finally { + hdrs.close(); + } + } + } + + /* + * Outside the synchronization block... + * + * Do we need to fetch the entire mesage content in order to + * load the headers as a side effect? Yes, there's a race + * condition here - multiple threads could decide that the + * content needs to be fetched. Fortunately, they'll all + * synchronize in the getContentStream method and the content + * will only be loaded once. + */ + if (fetchContent) { + InputStream cs = null; + try { + cs = getContentStream(); + } finally { + if (cs != null) + cs.close(); + } + } + } catch (EOFException eex) { + folder.close(false); + throw new FolderClosedException(folder, eex.toString()); + } catch (IOException ex) { + throw new MessagingException("error loading POP3 headers", ex); + } + } +} diff --git a/app/src/main/java/com/sun/mail/pop3/POP3Provider.java b/app/src/main/java/com/sun/mail/pop3/POP3Provider.java new file mode 100644 index 0000000000..cf73a6c333 --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/POP3Provider.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import javax.mail.Provider; + +import com.sun.mail.util.DefaultProvider; + +/** + * The POP3 protocol provider. + */ +@DefaultProvider // Remove this annotation if you copy this provider +public class POP3Provider extends Provider { + public POP3Provider() { + super(Provider.Type.STORE, "pop3", POP3Store.class.getName(), + "Oracle", null); + } +} diff --git a/app/src/main/java/com/sun/mail/pop3/POP3SSLProvider.java b/app/src/main/java/com/sun/mail/pop3/POP3SSLProvider.java new file mode 100644 index 0000000000..4831c50e86 --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/POP3SSLProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import javax.mail.Provider; + +import com.sun.mail.util.DefaultProvider; + +/** + * The POP3 SSL protocol provider. + */ +@DefaultProvider // Remove this annotation if you copy this provider +public class POP3SSLProvider extends Provider { + public POP3SSLProvider() { + super(Provider.Type.STORE, "pop3s", POP3SSLStore.class.getName(), + "Oracle", null); + } +} diff --git a/app/src/main/java/com/sun/mail/pop3/POP3SSLStore.java b/app/src/main/java/com/sun/mail/pop3/POP3SSLStore.java new file mode 100644 index 0000000000..fa0054c508 --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/POP3SSLStore.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import javax.mail.*; + +/** + * A POP3 Message Store using SSL. Contains only one folder, "INBOX". + * + * @author Bill Shannon + */ +public class POP3SSLStore extends POP3Store { + + public POP3SSLStore(Session session, URLName url) { + super(session, url, "pop3s", true); + } +} diff --git a/app/src/main/java/com/sun/mail/pop3/POP3Store.java b/app/src/main/java/com/sun/mail/pop3/POP3Store.java new file mode 100644 index 0000000000..c9818a1de7 --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/POP3Store.java @@ -0,0 +1,429 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.util.Properties; +import java.util.logging.Level; +import java.lang.reflect.*; + +import javax.mail.*; +import javax.mail.internet.*; +import java.io.File; +import java.io.PrintStream; +import java.io.IOException; +import java.io.EOFException; +import java.util.Collections; +import java.util.Map; + +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.SocketConnectException; +import com.sun.mail.util.MailConnectException; + +/** + * A POP3 Message Store. Contains only one folder, "INBOX". + * + * See the com.sun.mail.pop3 package + * documentation for further information on the POP3 protocol provider.

+ * + * @author Bill Shannon + * @author John Mani + */ +public class POP3Store extends Store { + + private String name = "pop3"; // my protocol name + private int defaultPort = 110; // default POP3 port + private boolean isSSL = false; // use SSL? + + private Protocol port = null; // POP3 port for self + private POP3Folder portOwner = null; // folder owning port + private String host = null; // host + private int portNum = -1; + private String user = null; + private String passwd = null; + private boolean useStartTLS = false; + private boolean requireStartTLS = false; + private boolean usingSSL = false; + private Map capabilities; + private MailLogger logger; + + // following set here and accessed by other classes in this package + volatile Constructor messageConstructor = null; + volatile boolean rsetBeforeQuit = false; + volatile boolean disableTop = false; + volatile boolean forgetTopHeaders = false; + volatile boolean supportsUidl = true; + volatile boolean cacheWriteTo = false; + volatile boolean useFileCache = false; + volatile File fileCacheDir = null; + volatile boolean keepMessageContent = false; + volatile boolean finalizeCleanClose = false; + + public POP3Store(Session session, URLName url) { + this(session, url, "pop3", false); + } + + public POP3Store(Session session, URLName url, + String name, boolean isSSL) { + super(session, url); + if (url != null) + name = url.getProtocol(); + this.name = name; + logger = new MailLogger(this.getClass(), "DEBUG POP3", + session.getDebug(), session.getDebugOut()); + + if (!isSSL) + isSSL = PropUtil.getBooleanProperty(session.getProperties(), + "mail." + name + ".ssl.enable", false); + if (isSSL) + this.defaultPort = 995; + else + this.defaultPort = 110; + this.isSSL = isSSL; + + rsetBeforeQuit = getBoolProp("rsetbeforequit"); + disableTop = getBoolProp("disabletop"); + forgetTopHeaders = getBoolProp("forgettopheaders"); + cacheWriteTo = getBoolProp("cachewriteto"); + useFileCache = getBoolProp("filecache.enable"); + String dir = session.getProperty("mail." + name + ".filecache.dir"); + if (dir != null && logger.isLoggable(Level.CONFIG)) + logger.config("mail." + name + ".filecache.dir: " + dir); + if (dir != null) + fileCacheDir = new File(dir); + keepMessageContent = getBoolProp("keepmessagecontent"); + + // mail.pop3.starttls.enable enables use of STLS command + useStartTLS = getBoolProp("starttls.enable"); + + // mail.pop3.starttls.required requires use of STLS command + requireStartTLS = getBoolProp("starttls.required"); + + // mail.pop3.finalizecleanclose requires clean close when finalizing + finalizeCleanClose = getBoolProp("finalizecleanclose"); + + String s = session.getProperty("mail." + name + ".message.class"); + if (s != null) { + logger.log(Level.CONFIG, "message class: {0}", s); + try { + ClassLoader cl = this.getClass().getClassLoader(); + + // now load the class + Class messageClass = null; + try { + // First try the "application's" class loader. + // This should eventually be replaced by + // Thread.currentThread().getContextClassLoader(). + messageClass = Class.forName(s, false, cl); + } catch (ClassNotFoundException ex1) { + // That didn't work, now try the "system" class loader. + // (Need both of these because JDK 1.1 class loaders + // may not delegate to their parent class loader.) + messageClass = Class.forName(s); + } + + Class[] c = {javax.mail.Folder.class, int.class}; + messageConstructor = messageClass.getConstructor(c); + } catch (Exception ex) { + logger.log(Level.CONFIG, "failed to load message class", ex); + } + } + } + + /** + * Get the value of a boolean property. + * Print out the value if logging is enabled. + */ + private final synchronized boolean getBoolProp(String prop) { + prop = "mail." + name + "." + prop; + boolean val = PropUtil.getBooleanProperty(session.getProperties(), + prop, false); + if (logger.isLoggable(Level.CONFIG)) + logger.config(prop + ": " + val); + return val; + } + + /** + * Get a reference to the session. + */ + synchronized Session getSession() { + return session; + } + + @Override + protected synchronized boolean protocolConnect(String host, int portNum, + String user, String passwd) throws MessagingException { + + // check for non-null values of host, password, user + if (host == null || passwd == null || user == null) + return false; + + // if port is not specified, set it to value of mail.pop3.port + // property if it exists, otherwise default to 110 + if (portNum == -1) + portNum = PropUtil.getIntProperty(session.getProperties(), + "mail." + name + ".port", -1); + + if (portNum == -1) + portNum = defaultPort; + + this.host = host; + this.portNum = portNum; + this.user = user; + this.passwd = passwd; + try { + port = getPort(null); + } catch (EOFException eex) { + throw new AuthenticationFailedException(eex.getMessage()); + } catch (SocketConnectException scex) { + throw new MailConnectException(scex); + } catch (IOException ioex) { + throw new MessagingException("Connect failed", ioex); + } + + return true; + } + + /** + * Check whether this store is connected. Override superclass + * method, to actually ping our server connection. + */ + /* + * Note that we maintain somewhat of an illusion of being connected + * even if we're not really connected. This is because a Folder + * can use the connection and close it when it's done. If we then + * ask whether the Store's connected we want the answer to be true, + * as long as we can reconnect at that point. This means that we + * need to be able to reconnect the Store on demand. + */ + @Override + public synchronized boolean isConnected() { + if (!super.isConnected()) + // if we haven't been connected at all, don't bother with + // the NOOP. + return false; + try { + if (port == null) + port = getPort(null); + else if (!port.noop()) + throw new IOException("NOOP failed"); + return true; + } catch (IOException ioex) { + // no longer connected, close it down + try { + super.close(); // notifies listeners + } catch (MessagingException mex) { + // ignore it + } + return false; + } + } + + synchronized Protocol getPort(POP3Folder owner) throws IOException { + Protocol p; + + // if we already have a port, remember who's using it + if (port != null && portOwner == null) { + portOwner = owner; + return port; + } + + // need a new port, create it and try to login + p = new Protocol(host, portNum, logger, + session.getProperties(), "mail." + name, isSSL); + + if (useStartTLS || requireStartTLS) { + if (p.hasCapability("STLS")) { + if (p.stls()) { + // success, refresh capabilities + p.setCapabilities(p.capa()); + } else if (requireStartTLS) { + logger.fine("STLS required but failed"); + throw cleanupAndThrow(p, + new EOFException("STLS required but failed")); + } + } else if (requireStartTLS) { + logger.fine("STLS required but not supported"); + throw cleanupAndThrow(p, + new EOFException("STLS required but not supported")); + } + } + + capabilities = p.getCapabilities(); // save for later, may be null + usingSSL = p.isSSL(); // in case anyone asks + + /* + * If we haven't explicitly disabled use of the TOP command, + * and the server has provided its capabilities, + * and the server doesn't support the TOP command, + * disable the TOP command. + */ + if (!disableTop && + capabilities != null && !capabilities.containsKey("TOP")) { + disableTop = true; + logger.fine("server doesn't support TOP, disabling it"); + } + + supportsUidl = capabilities == null || capabilities.containsKey("UIDL"); + + String msg = null; + if ((msg = p.login(user, passwd)) != null) { + throw cleanupAndThrow(p, new EOFException(msg)); + } + + /* + * If a Folder closes the port, and then a Folder + * is opened, the Store won't have a port. In that + * case, the getPort call will come from Folder.open, + * but we need to keep track of the port in the Store + * so that a later call to Folder.isOpen, which calls + * Store.isConnected, will use the same port. + */ + if (port == null && owner != null) { + port = p; + portOwner = owner; + } + if (portOwner == null) + portOwner = owner; + return p; + } + + private static IOException cleanupAndThrow(Protocol p, IOException ife) { + try { + p.quit(); + } catch (Throwable thr) { + if (isRecoverable(thr)) { + ife.addSuppressed(thr); + } else { + thr.addSuppressed(ife); + if (thr instanceof Error) { + throw (Error) thr; + } + if (thr instanceof RuntimeException) { + throw (RuntimeException) thr; + } + throw new RuntimeException("unexpected exception", thr); + } + } + return ife; + } + + private static boolean isRecoverable(Throwable t) { + return (t instanceof Exception) || (t instanceof LinkageError); + } + + synchronized void closePort(POP3Folder owner) { + if (portOwner == owner) { + port = null; + portOwner = null; + } + } + + @Override + public synchronized void close() throws MessagingException { + close(false); + } + + synchronized void close(boolean force) throws MessagingException { + try { + if (port != null) { + if (force) + port.close(); + else + port.quit(); + } + } catch (IOException ioex) { + } finally { + port = null; + + // to set the state and send the closed connection event + super.close(); + } + } + + @Override + public Folder getDefaultFolder() throws MessagingException { + checkConnected(); + return new DefaultFolder(this); + } + + /** + * Only the name "INBOX" is supported. + */ + @Override + public Folder getFolder(String name) throws MessagingException { + checkConnected(); + return new POP3Folder(this, name); + } + + @Override + public Folder getFolder(URLName url) throws MessagingException { + checkConnected(); + return new POP3Folder(this, url.getFile()); + } + + /** + * Return a Map of the capabilities the server provided, + * as per RFC 2449. If the server doesn't support RFC 2449, + * an emtpy Map is returned. The returned Map can not be modified. + * The key to the Map is the upper case capability name as + * a String. The value of the entry is the entire String + * capability line returned by the server.

+ * + * For example, to check if the server supports the STLS capability, use: + * if (store.capabilities().containsKey("STLS")) ... + * + * @return Map of capabilities + * @exception MessagingException for failures + * @since JavaMail 1.4.3 + */ + public Map capabilities() throws MessagingException { + Map c; + synchronized (this) { + c = capabilities; + } + if (c != null) + return Collections.unmodifiableMap(c); + else + return Collections.emptyMap(); + } + + /** + * Is this POP3Store using SSL to connect to the server? + * + * @return true if using SSL + * @since JavaMail 1.4.6 + */ + public synchronized boolean isSSL() { + return usingSSL; + } + + @Override + protected void finalize() throws Throwable { + try { + if (port != null) // don't force a connection attempt + close(!finalizeCleanClose); + } finally { + super.finalize(); + } + } + + private void checkConnected() throws MessagingException { + if (!super.isConnected()) + throw new MessagingException("Not connected"); + } +} diff --git a/app/src/main/java/com/sun/mail/pop3/Protocol.java b/app/src/main/java/com/sun/mail/pop3/Protocol.java new file mode 100644 index 0000000000..85c9a0f953 --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/Protocol.java @@ -0,0 +1,862 @@ +/* + * Copyright (c) 1997, 2020 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.util.*; +import java.net.*; +import java.io.*; +import java.security.*; +import java.util.logging.Level; +import javax.net.ssl.SSLSocket; + +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.SocketFetcher; +import com.sun.mail.util.LineInputStream; +import com.sun.mail.util.TraceInputStream; +import com.sun.mail.util.TraceOutputStream; +import com.sun.mail.util.SharedByteArrayOutputStream; + +class Response { + boolean ok = false; // true if "+OK" + String data = null; // rest of line after "+OK" or "-ERR" + InputStream bytes = null; // all the bytes from a multi-line response +} + +/** + * This class provides a POP3 connection and implements + * the POP3 protocol requests. + * + * APOP support courtesy of "chamness". + * + * @author Bill Shannon + */ +class Protocol { + private Socket socket; // POP3 socket + private String host; // host we're connected to + private Properties props; // session properties + private String prefix; // protocol name prefix, for props + private BufferedReader input; // input buf + private PrintWriter output; // output buf + private TraceInputStream traceInput; + private TraceOutputStream traceOutput; + private MailLogger logger; + private MailLogger traceLogger; + private String apopChallenge = null; + private Map capabilities = null; + private boolean pipelining; + private boolean noauthdebug = true; // hide auth info in debug output + private boolean traceSuspended; // temporarily suspend tracing + + private static final int POP3_PORT = 110; // standard POP3 port + private static final String CRLF = "\r\n"; + // sometimes the returned size isn't quite big enough + private static final int SLOP = 128; + + /** + * Open a connection to the POP3 server. + */ + Protocol(String host, int port, MailLogger logger, + Properties props, String prefix, boolean isSSL) + throws IOException { + this.host = host; + this.props = props; + this.prefix = prefix; + this.logger = logger; + traceLogger = logger.getSubLogger("protocol", null); + noauthdebug = !PropUtil.getBooleanProperty(props, + "mail.debug.auth", false); + + Response r; + boolean enableAPOP = getBoolProp(props, prefix + ".apop.enable"); + boolean disableCapa = getBoolProp(props, prefix + ".disablecapa"); + try { + if (port == -1) + port = POP3_PORT; + if (logger.isLoggable(Level.FINE)) + logger.fine("connecting to host \"" + host + + "\", port " + port + ", isSSL " + isSSL); + + socket = SocketFetcher.getSocket(host, port, props, prefix, isSSL); + initStreams(); + r = simpleCommand(null); + } catch (IOException ioe) { + throw cleanupAndThrow(socket, ioe); + } + + if (!r.ok) { + throw cleanupAndThrow(socket, new IOException("Connect failed")); + } + if (enableAPOP && r.data != null) { + int challStart = r.data.indexOf('<'); // start of challenge + int challEnd = r.data.indexOf('>', challStart); // end of challenge + if (challStart != -1 && challEnd != -1) + apopChallenge = r.data.substring(challStart, challEnd + 1); + logger.log(Level.FINE, "APOP challenge: {0}", apopChallenge); + } + + // if server supports RFC 2449, set capabilities + if (!disableCapa) + setCapabilities(capa()); + + pipelining = hasCapability("PIPELINING") || + PropUtil.getBooleanProperty(props, prefix + ".pipelining", false); + if (pipelining) + logger.config("PIPELINING enabled"); + } + + private static IOException cleanupAndThrow(Socket socket, IOException ife) { + try { + socket.close(); + } catch (Throwable thr) { + if (isRecoverable(thr)) { + ife.addSuppressed(thr); + } else { + thr.addSuppressed(ife); + if (thr instanceof Error) { + throw (Error) thr; + } + if (thr instanceof RuntimeException) { + throw (RuntimeException) thr; + } + throw new RuntimeException("unexpected exception", thr); + } + } + return ife; + } + + private static boolean isRecoverable(Throwable t) { + return (t instanceof Exception) || (t instanceof LinkageError); + } + + /** + * Get the value of a boolean property. + * Print out the value if logging is enabled. + */ + private final synchronized boolean getBoolProp(Properties props, + String prop) { + boolean val = PropUtil.getBooleanProperty(props, prop, false); + if (logger.isLoggable(Level.CONFIG)) + logger.config(prop + ": " + val); + return val; + } + + private void initStreams() throws IOException { + boolean quote = PropUtil.getBooleanProperty(props, + "mail.debug.quote", false); + traceInput = + new TraceInputStream(socket.getInputStream(), traceLogger); + traceInput.setQuote(quote); + + traceOutput = + new TraceOutputStream(socket.getOutputStream(), traceLogger); + traceOutput.setQuote(quote); + + // should be US-ASCII, but not all JDK's support it so use iso-8859-1 + input = new BufferedReader(new InputStreamReader(traceInput, + "iso-8859-1")); + output = new PrintWriter( + new BufferedWriter( + new OutputStreamWriter(traceOutput, "iso-8859-1"))); + } + + @Override + protected void finalize() throws Throwable { + try { + if (socket != null) // Forgot to logout ?! + quit(); + } finally { + super.finalize(); + } + } + + /** + * Parse the capabilities from a CAPA response. + */ + synchronized void setCapabilities(InputStream in) { + if (in == null) { + capabilities = null; + return; + } + + capabilities = new HashMap<>(10); + BufferedReader r = null; + try { + r = new BufferedReader(new InputStreamReader(in, "us-ascii")); + } catch (UnsupportedEncodingException ex) { + // should never happen + assert false; + } + String s; + try { + while ((s = r.readLine()) != null) { + String cap = s; + int i = cap.indexOf(' '); + if (i > 0) + cap = cap.substring(0, i); + capabilities.put(cap.toUpperCase(Locale.ENGLISH), s); + } + } catch (IOException ex) { + // should never happen + } finally { + try { + in.close(); + } catch (IOException ex) { } + } + } + + /** + * Check whether the given capability is supported by + * this server. Returns true if so, otherwise + * returns false. + */ + synchronized boolean hasCapability(String c) { + return capabilities != null && + capabilities.containsKey(c.toUpperCase(Locale.ENGLISH)); + } + + /** + * Return the map of capabilities returned by the server. + */ + synchronized Map getCapabilities() { + return capabilities; + } + + /** + * Login to the server, using the USER and PASS commands. + */ + synchronized String login(String user, String password) + throws IOException { + Response r; + // only pipeline password if connection is secure + boolean batch = pipelining && socket instanceof SSLSocket; + + try { + + if (noauthdebug && isTracing()) { + logger.fine("authentication command trace suppressed"); + suspendTracing(); + } + String dpw = null; + if (apopChallenge != null) + dpw = getDigest(password); + if (apopChallenge != null && dpw != null) { + r = simpleCommand("APOP " + user + " " + dpw); + } else if (batch) { + String cmd = "USER " + user; + batchCommandStart(cmd); + issueCommand(cmd); + cmd = "PASS " + password; + batchCommandContinue(cmd); + issueCommand(cmd); + r = readResponse(); + if (!r.ok) { + String err = r.data != null ? r.data : "USER command failed"; + readResponse(); // read and ignore PASS response + batchCommandEnd(); + return err; + } + r = readResponse(); + batchCommandEnd(); + } else { + r = simpleCommand("USER " + user); + if (!r.ok) + return r.data != null ? r.data : "USER command failed"; + r = simpleCommand("PASS " + password); + } + if (noauthdebug && isTracing()) + logger.log(Level.FINE, "authentication command {0}", + (r.ok ? "succeeded" : "failed")); + if (!r.ok) + return r.data != null ? r.data : "login failed"; + return null; + + } finally { + resumeTracing(); + } + } + + /** + * Gets the APOP message digest. + * From RFC 1939: + * + * The 'digest' parameter is calculated by applying the MD5 + * algorithm [RFC1321] to a string consisting of the timestamp + * (including angle-brackets) followed by a shared secret. + * The 'digest' parameter itself is a 16-octet value which is + * sent in hexadecimal format, using lower-case ASCII characters. + * + * @param password The APOP password + * @return The APOP digest or an empty string if an error occurs. + */ + private String getDigest(String password) { + String key = apopChallenge + password; + byte[] digest; + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + digest = md.digest(key.getBytes("iso-8859-1")); // XXX + } catch (NoSuchAlgorithmException nsae) { + return null; + } catch (UnsupportedEncodingException uee) { + return null; + } + return toHex(digest); + } + + private static char[] digits = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + + /** + * Convert a byte array to a string of hex digits representing the bytes. + */ + private static String toHex(byte[] bytes) { + char[] result = new char[bytes.length * 2]; + + for (int index = 0, i = 0; index < bytes.length; index++) { + int temp = bytes[index] & 0xFF; + result[i++] = digits[temp >> 4]; + result[i++] = digits[temp & 0xF]; + } + return new String(result); + } + + /** + * Close down the connection, sending the QUIT command. + */ + synchronized boolean quit() throws IOException { + boolean ok = false; + try { + Response r = simpleCommand("QUIT"); + ok = r.ok; + } finally { + close(); + } + return ok; + } + + /** + * Close the connection without sending any commands. + */ + void close() { + try { + if (socket != null) + socket.close(); + } catch (IOException ex) { + // ignore it + } finally { + socket = null; + input = null; + output = null; + } + } + + /** + * Return the total number of messages and mailbox size, + * using the STAT command. + */ + synchronized Status stat() throws IOException { + Response r = simpleCommand("STAT"); + Status s = new Status(); + + /* + * Normally the STAT command shouldn't fail but apparently it + * does when accessing Hotmail too often, returning: + * -ERR login allowed only every 15 minutes + * (Why it doesn't just fail the login, I don't know.) + * This is a serious failure that we don't want to hide + * from the user. + */ + if (!r.ok) + throw new IOException("STAT command failed: " + r.data); + + if (r.data != null) { + try { + StringTokenizer st = new StringTokenizer(r.data); + s.total = Integer.parseInt(st.nextToken()); + s.size = Integer.parseInt(st.nextToken()); + } catch (RuntimeException e) { + } + } + return s; + } + + /** + * Return the size of the message using the LIST command. + */ + synchronized int list(int msg) throws IOException { + Response r = simpleCommand("LIST " + msg); + int size = -1; + if (r.ok && r.data != null) { + try { + StringTokenizer st = new StringTokenizer(r.data); + st.nextToken(); // skip message number + size = Integer.parseInt(st.nextToken()); + } catch (RuntimeException e) { + // ignore it + } + } + return size; + } + + /** + * Return the size of all messages using the LIST command. + */ + synchronized InputStream list() throws IOException { + Response r = multilineCommand("LIST", 128); // 128 == output size est + return r.bytes; + } + + /** + * Retrieve the specified message. + * Given an estimate of the message's size we can be more efficient, + * preallocating the array and returning a SharedInputStream to allow + * us to share the array. + */ + synchronized InputStream retr(int msg, int size) throws IOException { + Response r; + String cmd; + boolean batch = size == 0 && pipelining; + if (batch) { + cmd = "LIST " + msg; + batchCommandStart(cmd); + issueCommand(cmd); + cmd = "RETR " + msg; + batchCommandContinue(cmd); + issueCommand(cmd); + r = readResponse(); + if (r.ok && r.data != null) { + // parse the LIST response to get the message size + try { + StringTokenizer st = new StringTokenizer(r.data); + st.nextToken(); // skip message number + size = Integer.parseInt(st.nextToken()); + // don't allow ridiculous sizes + if (size > 1024*1024*1024 || size < 0) + size = 0; + else { + if (logger.isLoggable(Level.FINE)) + logger.fine("pipeline message size " + size); + size += SLOP; + } + } catch (RuntimeException e) { + } + } + r = readResponse(); + if (r.ok) + r.bytes = readMultilineResponse(size + SLOP); + batchCommandEnd(); + } else { + cmd = "RETR " + msg; + multilineCommandStart(cmd); + issueCommand(cmd); + r = readResponse(); + if (!r.ok) { + multilineCommandEnd(); + return null; + } + + /* + * Many servers return a response to the RETR command of the form: + * +OK 832 octets + * If we don't have a size guess already, try to parse the response + * for data in that format and use it if found. It's only a guess, + * but it might be a good guess. + */ + if (size <= 0 && r.data != null) { + try { + StringTokenizer st = new StringTokenizer(r.data); + String s = st.nextToken(); + String octets = st.nextToken(); + if (octets.equals("octets")) { + size = Integer.parseInt(s); + // don't allow ridiculous sizes + if (size > 1024*1024*1024 || size < 0) + size = 0; + else { + if (logger.isLoggable(Level.FINE)) + logger.fine("guessing message size: " + size); + size += SLOP; + } + } + } catch (RuntimeException e) { + } + } + r.bytes = readMultilineResponse(size); + multilineCommandEnd(); + } + if (r.ok) { + if (size > 0 && logger.isLoggable(Level.FINE)) + logger.fine("got message size " + r.bytes.available()); + } + return r.bytes; + } + + /** + * Retrieve the specified message and stream the content to the + * specified OutputStream. Return true on success. + */ + synchronized boolean retr(int msg, OutputStream os) throws IOException { + String cmd = "RETR " + msg; + multilineCommandStart(cmd); + issueCommand(cmd); + Response r = readResponse(); + if (!r.ok) { + multilineCommandEnd(); + return false; + } + + Throwable terr = null; + int b, lastb = '\n'; + try { + while ((b = input.read()) >= 0) { + if (lastb == '\n' && b == '.') { + b = input.read(); + if (b == '\r') { + // end of response, consume LF as well + b = input.read(); + break; + } + } + + /* + * Keep writing unless we get an error while writing, + * which we defer until all of the data has been read. + */ + if (terr == null) { + try { + os.write(b); + } catch (IOException ex) { + logger.log(Level.FINE, "exception while streaming", ex); + terr = ex; + } catch (RuntimeException ex) { + logger.log(Level.FINE, "exception while streaming", ex); + terr = ex; + } + } + lastb = b; + } + } catch (InterruptedIOException iioex) { + /* + * As above in simpleCommand, close the socket to recover. + */ + try { + socket.close(); + } catch (IOException cex) { } + throw iioex; + } + if (b < 0) + throw new EOFException("EOF on socket"); + + // was there a deferred error? + if (terr != null) { + if (terr instanceof IOException) + throw (IOException)terr; + if (terr instanceof RuntimeException) + throw (RuntimeException)terr; + assert false; // can't get here + } + multilineCommandEnd(); + return true; + } + + /** + * Return the message header and the first n lines of the message. + */ + synchronized InputStream top(int msg, int n) throws IOException { + Response r = multilineCommand("TOP " + msg + " " + n, 0); + return r.bytes; + } + + /** + * Delete (permanently) the specified message. + */ + synchronized boolean dele(int msg) throws IOException { + Response r = simpleCommand("DELE " + msg); + return r.ok; + } + + /** + * Return the UIDL string for the message. + */ + synchronized String uidl(int msg) throws IOException { + Response r = simpleCommand("UIDL " + msg); + if (!r.ok) + return null; + int i = r.data.indexOf(' '); + if (i > 0) + return r.data.substring(i + 1); + else + return null; + } + + /** + * Return the UIDL strings for all messages. + * The UID for msg #N is returned in uids[N-1]. + */ + synchronized boolean uidl(String[] uids) throws IOException { + Response r = multilineCommand("UIDL", 15 * uids.length); + if (!r.ok) + return false; + LineInputStream lis = new LineInputStream(r.bytes); + String line = null; + while ((line = lis.readLine()) != null) { + int i = line.indexOf(' '); + if (i < 1 || i >= line.length()) + continue; + int n = Integer.parseInt(line.substring(0, i)); + if (n > 0 && n <= uids.length) + uids[n - 1] = line.substring(i + 1); + } + try { + r.bytes.close(); + } catch (IOException ex) { + // ignore it + } + return true; + } + + /** + * Do a NOOP. + */ + synchronized boolean noop() throws IOException { + Response r = simpleCommand("NOOP"); + return r.ok; + } + + /** + * Do an RSET. + */ + synchronized boolean rset() throws IOException { + Response r = simpleCommand("RSET"); + return r.ok; + } + + /** + * Start TLS using STLS command specified by RFC 2595. + * If already using SSL, this is a nop and the STLS command is not issued. + */ + synchronized boolean stls() throws IOException { + if (socket instanceof SSLSocket) + return true; // nothing to do + Response r = simpleCommand("STLS"); + if (r.ok) { + // it worked, now switch the socket into TLS mode + try { + socket = SocketFetcher.startTLS(socket, host, props, prefix); + initStreams(); + } catch (IOException ioex) { + try { + socket.close(); + } finally { + socket = null; + input = null; + output = null; + } + IOException sioex = + new IOException("Could not convert socket to TLS"); + sioex.initCause(ioex); + throw sioex; + } + } + return r.ok; + } + + /** + * Is this connection using SSL? + */ + synchronized boolean isSSL() { + return socket instanceof SSLSocket; + } + + /** + * Get server capabilities using CAPA command specified by RFC 2449. + * Returns null if not supported. + */ + synchronized InputStream capa() throws IOException { + Response r = multilineCommand("CAPA", 128); // 128 == output size est + if (!r.ok) + return null; + return r.bytes; + } + + /** + * Issue a simple POP3 command and return the response. + */ + private Response simpleCommand(String cmd) throws IOException { + simpleCommandStart(cmd); + issueCommand(cmd); + Response r = readResponse(); + simpleCommandEnd(); + return r; + } + + /** + * Send the specified command. + */ + private void issueCommand(String cmd) throws IOException { + if (socket == null) + throw new IOException("Folder is closed"); // XXX + + if (cmd != null) { + cmd += CRLF; + output.print(cmd); // do it in one write + output.flush(); + } + } + + /** + * Read the response to a command. + */ + private Response readResponse() throws IOException { + String line = null; + try { + line = input.readLine(); + } catch (InterruptedIOException iioex) { + /* + * If we get a timeout while using the socket, we have no idea + * what state the connection is in. The server could still be + * alive, but slow, and could still be sending data. The only + * safe way to recover is to drop the connection. + */ + try { + socket.close(); + } catch (IOException cex) { } + throw new EOFException(iioex.getMessage()); + } catch (SocketException ex) { + /* + * If we get an error while using the socket, we have no idea + * what state the connection is in. The server could still be + * alive, but slow, and could still be sending data. The only + * safe way to recover is to drop the connection. + */ + try { + socket.close(); + } catch (IOException cex) { } + throw new EOFException(ex.getMessage()); + } + + if (line == null) { + traceLogger.finest(""); + throw new EOFException("EOF on socket"); + } + Response r = new Response(); + if (line.startsWith("+OK")) + r.ok = true; + else if (line.startsWith("-ERR")) + r.ok = false; + else + throw new IOException("Unexpected response: " + line); + int i; + if ((i = line.indexOf(' ')) >= 0) + r.data = line.substring(i + 1); + return r; + } + + /** + * Issue a POP3 command that expects a multi-line response. + * size is an estimate of the response size. + */ + private Response multilineCommand(String cmd, int size) throws IOException { + multilineCommandStart(cmd); + issueCommand(cmd); + Response r = readResponse(); + if (!r.ok) { + multilineCommandEnd(); + return r; + } + r.bytes = readMultilineResponse(size); + multilineCommandEnd(); + return r; + } + + /** + * Read the response to a multiline command after the command response. + * The size parameter indicates the expected size of the response; + * the actual size can be different. Returns an InputStream to the + * response bytes. + */ + private InputStream readMultilineResponse(int size) throws IOException { + SharedByteArrayOutputStream buf = new SharedByteArrayOutputStream(size); + int b, lastb = '\n'; + try { + while ((b = input.read()) >= 0) { + if (lastb == '\n' && b == '.') { + b = input.read(); + if (b == '\r') { + // end of response, consume LF as well + b = input.read(); + break; + } + } + buf.write(b); + lastb = b; + } + } catch (InterruptedIOException iioex) { + /* + * As above in readResponse, close the socket to recover. + */ + try { + socket.close(); + } catch (IOException cex) { } + throw iioex; + } + if (b < 0) + throw new EOFException("EOF on socket"); + return buf.toStream(); + } + + /** + * Is protocol tracing enabled? + */ + protected boolean isTracing() { + return traceLogger.isLoggable(Level.FINEST); + } + + /** + * Temporarily turn off protocol tracing, e.g., to prevent + * tracing the authentication sequence, including the password. + */ + private void suspendTracing() { + if (traceLogger.isLoggable(Level.FINEST)) { + traceInput.setTrace(false); + traceOutput.setTrace(false); + } + } + + /** + * Resume protocol tracing, if it was enabled to begin with. + */ + private void resumeTracing() { + if (traceLogger.isLoggable(Level.FINEST)) { + traceInput.setTrace(true); + traceOutput.setTrace(true); + } + } + + /* + * Probe points for GlassFish monitoring. + */ + private void simpleCommandStart(String command) { } + private void simpleCommandEnd() { } + private void multilineCommandStart(String command) { } + private void multilineCommandEnd() { } + private void batchCommandStart(String command) { } + private void batchCommandContinue(String command) { } + private void batchCommandEnd() { } +} diff --git a/app/src/main/java/com/sun/mail/pop3/Status.java b/app/src/main/java/com/sun/mail/pop3/Status.java new file mode 100644 index 0000000000..bbfbd9cd9c --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/Status.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +/** + * Result of POP3 STAT command. + */ +class Status { + int total = 0; // number of messages in the mailbox + int size = 0; // size of the mailbox +}; diff --git a/app/src/main/java/com/sun/mail/pop3/TempFile.java b/app/src/main/java/com/sun/mail/pop3/TempFile.java new file mode 100644 index 0000000000..0d769e4aa5 --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/TempFile.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.io.*; + +/** + * A temporary file used to cache POP3 messages. + */ +class TempFile { + + private File file; // the temp file name + private WritableSharedFile sf; + + /** + * Create a temp file in the specified directory (if not null). + * The file will be deleted when the JVM exits. + */ + public TempFile(File dir) throws IOException { + file = File.createTempFile("pop3.", ".mbox", dir); + // XXX - need JDK 6 to set permissions on the file to owner-only + file.deleteOnExit(); + sf = new WritableSharedFile(file); + } + + /** + * Return a stream for appending to the temp file. + */ + public AppendStream getAppendStream() throws IOException { + return sf.getAppendStream(); + } + + /** + * Close and remove this temp file. + */ + public void close() { + try { + sf.close(); + } catch (IOException ex) { + // ignore it + } + file.delete(); + } + + @Override + protected void finalize() throws Throwable { + try { + close(); + } finally { + super.finalize(); + } + } +} diff --git a/app/src/main/java/com/sun/mail/pop3/WritableSharedFile.java b/app/src/main/java/com/sun/mail/pop3/WritableSharedFile.java new file mode 100644 index 0000000000..8daa0c5378 --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/WritableSharedFile.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import javax.mail.util.SharedFileInputStream; + +/** + * A subclass of SharedFileInputStream that also allows writing. + */ +class WritableSharedFile extends SharedFileInputStream { + + private RandomAccessFile raf; + private AppendStream af; + + public WritableSharedFile(File file) throws IOException { + super(file); + try { + raf = new RandomAccessFile(file, "rw"); + } catch (IOException ex) { + // if anything goes wrong opening the writable file, + // close the readable file too + super.close(); + } + } + + /** + * Return the writable version of this file. + */ + public RandomAccessFile getWritableFile() { + return raf; + } + + /** + * Close the readable and writable files. + */ + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + raf.close(); + } + } + + /** + * Update the size of the readable file after writing to the file. Updates + * the length to be the current size of the file. + */ + synchronized long updateLength() throws IOException { + datalen = in.length(); + af = null; + return datalen; + } + + /** + * Return a new AppendStream, but only if one isn't in active use. + */ + public synchronized AppendStream getAppendStream() throws IOException { + if (af != null) { + throw new IOException( + "POP3 file cache only supports single threaded access"); + } + af = new AppendStream(this); + return af; + } +} diff --git a/app/src/main/java/com/sun/mail/pop3/package.html b/app/src/main/java/com/sun/mail/pop3/package.html new file mode 100644 index 0000000000..a3ea13b8eb --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/package.html @@ -0,0 +1,674 @@ + + + + + + +com.sun.mail.pop3 package + + + +A POP3 protocol provider for the Jakarta Mail API +that provides access to a POP3 message store. +Refer to +RFC 1939 +for more information. +

+The POP3 provider provides a Store object that contains a single Folder +named "INBOX". Due to the limitations of the POP3 protocol, many of +the Jakarta Mail API capabilities like event notification, folder management, +flag management, etc. are not allowed. The corresponding methods throw +the MethodNotSupportedException exception; see below for details. +

+

+Note that Jakarta Mail does not include a local store into +which messages can be downloaded and stored. See our + +Third Party Products +web page for availability of "mbox" and "MH" local store providers. +

+

+The POP3 provider is accessed through the Jakarta Mail APIs by using the protocol +name "pop3" or a URL of the form "pop3://user:password@host:port/INBOX". +

+

+POP3 supports only a single folder named "INBOX". +

+

+POP3 supports no permanent flags (see +{@link javax.mail.Folder#getPermanentFlags Folder.getPermanentFlags()}). +In particular, the Flags.Flag.RECENT flag will never be set +for POP3 +messages. It's up to the application to determine which messages in a +POP3 mailbox are "new". There are several strategies to accomplish +this, depending on the needs of the application and the environment: +

+
    +
  • +A simple approach would be to keep track of the newest +message seen by the application. +
  • +
  • +An alternative would be to keep track of the UIDs (see below) +of all messages that have been seen. +
  • +
  • +Another approach is to download all messages into a local +mailbox, so that all messages in the POP3 mailbox are, by +definition, new. +
  • +
+

+All approaches will require some permanent storage associated with the client. +

+

+POP3 does not support the Folder.expunge() method. To delete and +expunge messages, set the Flags.Flag.DELETED flag on the messages +and close the folder using the Folder.close(true) method. You +cannot expunge without closing the folder. +

+

+POP3 does not provide a "received date", so the getReceivedDate +method will return null. +It may be possible to examine other message headers (e.g., the +"Received" headers) to estimate the received date, but these techniques +are error-prone at best. +

+

+The POP3 provider supports the POP3 UIDL command, see +{@link com.sun.mail.pop3.POP3Folder#getUID POP3Folder.getUID()}. +You can use it as follows: +

+
+if (folder instanceof com.sun.mail.pop3.POP3Folder) {
+    com.sun.mail.pop3.POP3Folder pf =
+	(com.sun.mail.pop3.POP3Folder)folder;
+    String uid = pf.getUID(msg);
+    if (uid != null)
+	... // use it
+}
+
+

+You can also pre-fetch all the UIDs for all messages like this: +

+
+FetchProfile fp = new FetchProfile();
+fp.add(UIDFolder.FetchProfileItem.UID);
+folder.fetch(folder.getMessages(), fp);
+
+

+Then use the technique above to get the UID for each message. This is +similar to the technique used with the UIDFolder interface supported by +IMAP, but note that POP3 UIDs are strings, not integers like IMAP +UIDs. See the POP3 spec for details. +

+

+When the headers of a POP3 message are accessed, the POP3 provider uses +the TOP command to fetch all headers, which are then cached. Use of the +TOP command can be disabled with the mail.pop3.disabletop +property, in which case the entire message content is fetched with the +RETR command. +

+

+When the content of a POP3 message is accessed, the POP3 provider uses +the RETR command to fetch the entire message. Normally the message +content is cached in memory. By setting the +mail.pop3.filecache.enable property, the message content +will instead be cached in a temporary file. The file will be removed +when the folder is closed. Caching message content in a file is generally +slower, but uses substantially less memory and may be helpful when dealing +with very large messages. +

+

+The {@link com.sun.mail.pop3.POP3Message#invalidate POP3Message.invalidate} +method can be used to invalidate cached data without closing the folder. +Note that if the file cache is being used the data in the file will be +forgotten and fetched from the server if it's needed again, and stored again +in the file cache. +

+

+The POP3 CAPA command (defined by +RFC 2449) +will be used to determine the capabilities supported by the server. +Some servers don't implement the CAPA command, and some servers don't +return correct information, so various properties are available to +disable use of certain POP3 commands, including CAPA. +

+

+If the server advertises the PIPELINING capability (defined by +RFC 2449), +or the mail.pop3.pipelining property is set, the POP3 +provider will send some commands in batches, which can significantly +improve performance and memory use. +Some servers that don't support the CAPA command or don't advertise +PIPELINING may still support pipelining; experimentation may be required. +

+

+If pipelining is supported and the connection is using +SSL, the USER and PASS commands will be sent as a batch. +(If SSL is not being used, the PASS command isn't sent +until the user is verified to avoid exposing the password +if the user name is bad.) +

+

+If pipelining is supported, when fetching a message with the RETR command, +the LIST command will be sent as well, and the result will be used to size +the I/O buffer, greatly reducing memory usage when fetching messages. +

+Properties +

+The POP3 protocol provider supports the following properties, +which may be set in the Jakarta Mail Session object. +The properties are always set as strings; the Type column describes +how the string is interpreted. For example, use +

+
+	props.put("mail.pop3.port", "888");
+
+

+to set the mail.pop3.port property, which is of type int. +

+

+Note that if you're using the "pop3s" protocol to access POP3 over SSL, +all the properties would be named "mail.pop3s.*". +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
POP3 properties
NameTypeDescription
mail.pop3.userStringDefault user name for POP3.
mail.pop3.hostStringThe POP3 server to connect to.
mail.pop3.portintThe POP3 server port to connect to, if the connect() method doesn't +explicitly specify one. Defaults to 110.
mail.pop3.connectiontimeoutintSocket connection timeout value in milliseconds. +This timeout is implemented by java.net.Socket. +Default is infinite timeout.
mail.pop3.timeoutintSocket read timeout value in milliseconds. +This timeout is implemented by java.net.Socket. +Default is infinite timeout.
mail.pop3.writetimeoutintSocket write timeout value in milliseconds. +This timeout is implemented by using a +java.util.concurrent.ScheduledExecutorService per connection +that schedules a thread to close the socket if the timeout expires. +Thus, the overhead of using this timeout is one thread per connection. +Default is infinite timeout.
mail.pop3.rsetbeforequitboolean +Send a POP3 RSET command when closing the folder, before sending the +QUIT command. Useful with POP3 servers that implicitly mark all +messages that are read as "deleted"; this will prevent such messages +from being deleted and expunged unless the client requests so. Default +is false. +
mail.pop3.message.classString +Class name of a subclass of com.sun.mail.pop3.POP3Message. +The subclass can be used to handle (for example) non-standard +Content-Type headers. The subclass must have a public constructor +of the form MyPOP3Message(Folder f, int msgno) +throws MessagingException. +
mail.pop3.localaddressString +Local address (host name) to bind to when creating the POP3 socket. +Defaults to the address picked by the Socket class. +Should not normally need to be set, but useful with multi-homed hosts +where it's important to pick a particular local address to bind to. +
mail.pop3.localportint +Local port number to bind to when creating the POP3 socket. +Defaults to the port number picked by the Socket class. +
mail.pop3.apop.enableboolean +If set to true, use APOP instead of USER/PASS to login to the +POP3 server, if the POP3 server supports APOP. APOP sends a +digest of the password rather than the clear text password. +Defaults to false. +
mail.pop3.socketFactorySocketFactory +If set to a class that implements the +javax.net.SocketFactory interface, this class +will be used to create POP3 sockets. Note that this is an +instance of a class, not a name, and must be set using the +put method, not the setProperty method. +
mail.pop3.socketFactory.classString +If set, specifies the name of a class that implements the +javax.net.SocketFactory interface. This class +will be used to create POP3 sockets. +
mail.pop3.socketFactory.fallbackboolean +If set to true, failure to create a socket using the specified +socket factory class will cause the socket to be created using +the java.net.Socket class. +Defaults to true. +
mail.pop3.socketFactory.portint +Specifies the port to connect to when using the specified socket +factory. +If not set, the default port will be used. +
mail.pop3.ssl.enableboolean +If set to true, use SSL to connect and use the SSL port by default. +Defaults to false for the "pop3" protocol and true for the "pop3s" protocol. +
mail.pop3.ssl.checkserveridentityboolean +If set to true, check the server identity as specified by +RFC 2595. +These additional checks based on the content of the server's certificate +are intended to prevent man-in-the-middle attacks. +Defaults to false. +
mail.pop3.ssl.trustString +If set, and a socket factory hasn't been specified, enables use of a +{@link com.sun.mail.util.MailSSLSocketFactory MailSSLSocketFactory}. +If set to "*", all hosts are trusted. +If set to a whitespace separated list of hosts, those hosts are trusted. +Otherwise, trust depends on the certificate the server presents. +
mail.pop3.ssl.socketFactorySSLSocketFactory +If set to a class that extends the +javax.net.ssl.SSLSocketFactory class, this class +will be used to create POP3 SSL sockets. Note that this is an +instance of a class, not a name, and must be set using the +put method, not the setProperty method. +
mail.pop3.ssl.socketFactory.classString +If set, specifies the name of a class that extends the +javax.net.ssl.SSLSocketFactory class. This class +will be used to create POP3 SSL sockets. +
mail.pop3.ssl.socketFactory.portint +Specifies the port to connect to when using the specified socket +factory. +If not set, the default port will be used. +
mail.pop3.ssl.protocolsstring +Specifies the SSL protocols that will be enabled for SSL connections. +The property value is a whitespace separated list of tokens acceptable +to the javax.net.ssl.SSLSocket.setEnabledProtocols method. +
mail.pop3.ssl.ciphersuitesstring +Specifies the SSL cipher suites that will be enabled for SSL connections. +The property value is a whitespace separated list of tokens acceptable +to the javax.net.ssl.SSLSocket.setEnabledCipherSuites method. +
mail.pop3.starttls.enableboolean +If true, enables the use of the STLS command (if +supported by the server) to switch the connection to a TLS-protected +connection before issuing any login commands. +If the server does not support STARTTLS, the connection continues without +the use of TLS; see the +mail.pop3.starttls.required +property to fail if STARTTLS isn't supported. +Note that an appropriate trust store must configured so that the client +will trust the server's certificate. +Defaults to false. +
mail.pop3.starttls.requiredboolean +If true, requires the use of the STLS command. +If the server doesn't support the STLS command, or the command +fails, the connect method will fail. +Defaults to false. +
mail.pop3.proxy.hoststring +Specifies the host name of an HTTP web proxy server that will be used for +connections to the mail server. +
mail.pop3.proxy.portstring +Specifies the port number for the HTTP web proxy server. +Defaults to port 80. +
mail.pop3.proxy.userstring +Specifies the user name to use to authenticate with the HTTP web proxy server. +By default, no authentication is done. +
mail.pop3.proxy.passwordstring +Specifies the password to use to authenticate with the HTTP web proxy server. +By default, no authentication is done. +
mail.pop3.socks.hoststring +Specifies the host name of a SOCKS5 proxy server that will be used for +connections to the mail server. +
mail.pop3.socks.portstring +Specifies the port number for the SOCKS5 proxy server. +This should only need to be used if the proxy server is not using +the standard port number of 1080. +
mail.pop3.disabletopboolean +If set to true, the POP3 TOP command will not be used to fetch +message headers. This is useful for POP3 servers that don't +properly implement the TOP command, or that provide incorrect +information in the TOP command results. +Defaults to false. +
mail.pop3.disablecapaboolean +If set to true, the POP3 CAPA command will not be used to fetch +server capabilities. This is useful for POP3 servers that don't +properly implement the CAPA command, or that provide incorrect +information in the CAPA command results. +Defaults to false. +
mail.pop3.forgettopheadersboolean +If set to true, the headers that might have been retrieved using +the POP3 TOP command will be forgotten and replaced by headers +retrieved as part of the POP3 RETR command. Some servers, such +as some versions of Microsft Exchange and IBM Lotus Notes, +will return slightly different +headers each time the TOP or RETR command is used. To allow the +POP3 provider to properly parse the message content returned from +the RETR command, the headers also returned by the RETR command +must be used. Setting this property to true will cause these +headers to be used, even if they differ from the headers returned +previously as a result of using the TOP command. +Defaults to false. +
mail.pop3.filecache.enableboolean +If set to true, the POP3 provider will cache message data in a temporary +file rather than in memory. Messages are only added to the cache when +accessing the message content. Message headers are always cached in +memory (on demand). The file cache is removed when the folder is closed +or the JVM terminates. +Defaults to false. +
mail.pop3.filecache.dirString +If the file cache is enabled, this property can be used to override the +default directory used by the JDK for temporary files. +
mail.pop3.cachewritetoboolean +Controls the behavior of the +{@link com.sun.mail.pop3.POP3Message#writeTo writeTo} method +on a POP3 message object. +If set to true, and the message content hasn't yet been cached, +and ignoreList is null, the message is cached before being written. +Otherwise, the message is streamed directly +to the output stream without being cached. +Defaults to false. +
mail.pop3.keepmessagecontentboolean +The content of a message is cached when it is first fetched. +Normally this cache uses a {@link java.lang.ref.SoftReference SoftReference} +to refer to the cached content. This allows the cached content to be purged +if memory is low, in which case the content will be fetched again if it's +needed. +If this property is set to true, a hard reference to the cached content +will be kept, preventing the memory from being reused until the folder +is closed or the cached content is explicitly invalidated (using the +{@link com.sun.mail.pop3.POP3Message#invalidate invalidate} method). +(This was the behavior in previous versions of Jakarta Mail.) +Defaults to false. +
mail.pop3.finalizecleancloseboolean +When the finalizer for POP3Store or POP3Folder is called, +should the connection to the server be closed cleanly, as if the +application called the close method? +Or should the connection to the server be closed without sending +any commands to the server? +Defaults to false, the connection is not closed cleanly. +
+

+In general, applications should not need to use the classes in this +package directly. Instead, they should use the APIs defined by +javax.mail package (and subpackages). Applications should +never construct instances of POP3Store or +POP3Folder directly. Instead, they should use the +Session method getStore to acquire an +appropriate Store object, and from that acquire +Folder objects. +

+

+In addition to printing debugging output as controlled by the +{@link javax.mail.Session Session} configuration, +the com.sun.mail.pop3 provider logs the same information using +{@link java.util.logging.Logger} as described in the following table: +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
POP3 Loggers
Logger NameLogging LevelPurpose
com.sun.mail.pop3CONFIGConfiguration of the POP3Store
com.sun.mail.pop3FINEGeneral debugging output
com.sun.mail.pop3.protocolFINESTComplete protocol trace
+ +

+WARNING: The APIs unique to this package should be +considered EXPERIMENTAL. They may be changed in the +future in ways that are incompatible with applications using the +current APIs. +

+ + + diff --git a/app/src/main/java/com/sun/mail/smtp/DigestMD5.java b/app/src/main/java/com/sun/mail/smtp/DigestMD5.java new file mode 100644 index 0000000000..401834a0ce --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/DigestMD5.java @@ -0,0 +1,231 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.*; +import java.util.*; +import java.util.logging.Level; +import java.security.*; +import java.nio.charset.StandardCharsets; + +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.util.BASE64EncoderStream; +import com.sun.mail.util.BASE64DecoderStream; + +/** + * DIGEST-MD5 authentication support. + * + * @author Dean Gibson + * @author Bill Shannon + */ + +public class DigestMD5 { + + private MailLogger logger; + private MessageDigest md5; + private String uri; + private String clientResponse; + + public DigestMD5(MailLogger logger) { + this.logger = logger.getLogger(this.getClass(), "DEBUG DIGEST-MD5"); + logger.config("DIGEST-MD5 Loaded"); + } + + /** + * Return client's authentication response to server's challenge. + * + * @param host the host name + * @param user the user name + * @param passwd the user's password + * @param realm the security realm + * @param serverChallenge the challenge from the server + * @return byte array with client's response + * @exception IOException for I/O errors + */ + public byte[] authClient(String host, String user, String passwd, + String realm, String serverChallenge) + throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + OutputStream b64os = new BASE64EncoderStream(bos, Integer.MAX_VALUE); + SecureRandom random; + try { + //random = SecureRandom.getInstance("SHA1PRNG"); + random = new SecureRandom(); + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException ex) { + logger.log(Level.FINE, "NoSuchAlgorithmException", ex); + throw new IOException(ex.toString()); + } + StringBuilder result = new StringBuilder(); + + uri = "smtp/" + host; + String nc = "00000001"; + String qop = "auth"; + byte[] bytes = new byte[32]; // arbitrary size ... + int resp; + + logger.fine("Begin authentication ..."); + + // Code based on http://www.ietf.org/rfc/rfc2831.txt + Map map = tokenize(serverChallenge); + + if (realm == null) { + String text = map.get("realm"); + realm = text != null ? new StringTokenizer(text, ",").nextToken() + : host; + } + + // server challenge random value + String nonce = map.get("nonce"); + + // Does server support UTF-8 usernames and passwords? + String charset = map.get("charset"); + boolean utf8 = charset != null && charset.equalsIgnoreCase("utf-8"); + + random.nextBytes(bytes); + b64os.write(bytes); + b64os.flush(); + + // client challenge random value + String cnonce = bos.toString("iso-8859-1"); // really ASCII? + bos.reset(); + + // DIGEST-MD5 computation, common portion (order critical) + if (utf8) { + String up = user + ":" + realm + ":" + passwd; + md5.update(md5.digest(up.getBytes(StandardCharsets.UTF_8))); + } else + md5.update(md5.digest( + ASCIIUtility.getBytes(user + ":" + realm + ":" + passwd))); + md5.update(ASCIIUtility.getBytes(":" + nonce + ":" + cnonce)); + clientResponse = toHex(md5.digest()) + + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":"; + + // DIGEST-MD5 computation, client response (order critical) + md5.update(ASCIIUtility.getBytes("AUTHENTICATE:" + uri)); + md5.update(ASCIIUtility.getBytes(clientResponse + toHex(md5.digest()))); + + // build response text (order not critical) + result.append("username=\"" + user + "\""); + result.append(",realm=\"" + realm + "\""); + result.append(",qop=" + qop); + result.append(",nc=" + nc); + result.append(",nonce=\"" + nonce + "\""); + result.append(",cnonce=\"" + cnonce + "\""); + result.append(",digest-uri=\"" + uri + "\""); + if (utf8) + result.append(",charset=\"utf-8\""); + result.append(",response=" + toHex(md5.digest())); + + if (logger.isLoggable(Level.FINE)) + logger.fine("Response => " + result.toString()); + b64os.write(ASCIIUtility.getBytes(result.toString())); + b64os.flush(); + return bos.toByteArray(); + } + + /** + * Allow the client to authenticate the server based on its + * response. + * + * @param serverResponse the response that was received from the server + * @return true if server is authenticated + * @exception IOException for character conversion failures + */ + public boolean authServer(String serverResponse) throws IOException { + Map map = tokenize(serverResponse); + // DIGEST-MD5 computation, server response (order critical) + md5.update(ASCIIUtility.getBytes(":" + uri)); + md5.update(ASCIIUtility.getBytes(clientResponse + toHex(md5.digest()))); + String text = toHex(md5.digest()); + if (!text.equals(map.get("rspauth"))) { + if (logger.isLoggable(Level.FINE)) + logger.fine("Expected => rspauth=" + text); + return false; // server NOT authenticated by client !!! + } + return true; + } + + /** + * Tokenize a response from the server. + * + * @return Map containing key/value pairs from server + */ + @SuppressWarnings("fallthrough") + private Map tokenize(String serverResponse) + throws IOException { + Map map = new HashMap<>(); + byte[] bytes = serverResponse.getBytes("iso-8859-1"); // really ASCII? + String key = null; + int ttype; + StreamTokenizer tokens + = new StreamTokenizer( + new InputStreamReader( + new BASE64DecoderStream( + new ByteArrayInputStream(bytes, 4, bytes.length - 4) + ), "iso-8859-1" // really ASCII? + ) + ); + + tokens.ordinaryChars('0', '9'); // reset digits + tokens.wordChars('0', '9'); // digits may start words + while ((ttype = tokens.nextToken()) != StreamTokenizer.TT_EOF) { + switch (ttype) { + case StreamTokenizer.TT_WORD: + if (key == null) { + key = tokens.sval; + break; + } + // fall-thru + case '"': + if (logger.isLoggable(Level.FINE)) + logger.fine("Received => " + + key + "='" + tokens.sval + "'"); + if (map.containsKey(key)) { // concatenate multiple values + map.put(key, map.get(key) + "," + tokens.sval); + } else { + map.put(key, tokens.sval); + } + key = null; + break; + default: // XXX - should never happen? + break; + } + } + return map; + } + + private static char[] digits = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + + /** + * Convert a byte array to a string of hex digits representing the bytes. + */ + private static String toHex(byte[] bytes) { + char[] result = new char[bytes.length * 2]; + + for (int index = 0, i = 0; index < bytes.length; index++) { + int temp = bytes[index] & 0xFF; + result[i++] = digits[temp >> 4]; + result[i++] = digits[temp & 0xF]; + } + return new String(result); + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SMTPAddressFailedException.java b/app/src/main/java/com/sun/mail/smtp/SMTPAddressFailedException.java new file mode 100644 index 0000000000..234820e7cb --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SMTPAddressFailedException.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.SendFailedException; +import javax.mail.internet.InternetAddress; + +/** + * This exception is thrown when the message cannot be sent.

+ * + * The exception includes the address to which the message could not be + * sent. This will usually appear in a chained list of exceptions, + * one per address, attached to a top level SendFailedException that + * aggregates all the addresses. + * + * @since JavaMail 1.3.2 + */ + +public class SMTPAddressFailedException extends SendFailedException { + protected InternetAddress addr; // address that failed + protected String cmd; // command issued to server + protected int rc; // return code from SMTP server + + private static final long serialVersionUID = 804831199768630097L; + + /** + * Constructs an SMTPAddressFailedException with the specified + * address, return code, and error string. + * + * @param addr the address that failed + * @param cmd the command that was sent to the SMTP server + * @param rc the SMTP return code indicating the failure + * @param err the error string from the SMTP server + */ + public SMTPAddressFailedException(InternetAddress addr, String cmd, int rc, + String err) { + super(err); + this.addr = addr; + this.cmd = cmd; + this.rc = rc; + } + + /** + * Return the address that failed. + * + * @return the address + */ + public InternetAddress getAddress() { + return addr; + } + + /** + * Return the command that failed. + * + * @return the command + */ + public String getCommand() { + return cmd; + } + + + /** + * Return the return code from the SMTP server that indicates the + * reason for the failure. See + * RFC 821 + * for interpretation of the return code. + * + * @return the return code + */ + public int getReturnCode() { + return rc; + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SMTPAddressSucceededException.java b/app/src/main/java/com/sun/mail/smtp/SMTPAddressSucceededException.java new file mode 100644 index 0000000000..2ddb29a7eb --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SMTPAddressSucceededException.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.MessagingException; +import javax.mail.internet.InternetAddress; + +/** + * This exception is chained off a SendFailedException when the + * mail.smtp.reportsuccess property is true. It + * indicates an address to which the message was sent. The command + * will be an SMTP RCPT command and the return code will be the + * return code from that command. + * + * @since JavaMail 1.3.2 + */ + +public class SMTPAddressSucceededException extends MessagingException { + protected InternetAddress addr; // address that succeeded + protected String cmd; // command issued to server + protected int rc; // return code from SMTP server + + private static final long serialVersionUID = -1168335848623096749L; + + /** + * Constructs an SMTPAddressSucceededException with the specified + * address, return code, and error string. + * + * @param addr the address that succeeded + * @param cmd the command that was sent to the SMTP server + * @param rc the SMTP return code indicating the success + * @param err the error string from the SMTP server + */ + public SMTPAddressSucceededException(InternetAddress addr, + String cmd, int rc, String err) { + super(err); + this.addr = addr; + this.cmd = cmd; + this.rc = rc; + } + + /** + * Return the address that succeeded. + * + * @return the address + */ + public InternetAddress getAddress() { + return addr; + } + + /** + * Return the command that succeeded. + * + * @return the command + */ + public String getCommand() { + return cmd; + } + + + /** + * Return the return code from the SMTP server that indicates the + * reason for the success. See + * RFC 821 + * for interpretation of the return code. + * + * @return the return code + */ + public int getReturnCode() { + return rc; + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SMTPMessage.java b/app/src/main/java/com/sun/mail/smtp/SMTPMessage.java new file mode 100644 index 0000000000..826ea44ce0 --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SMTPMessage.java @@ -0,0 +1,321 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.*; +import javax.mail.*; +import javax.mail.internet.*; + +/** + * This class is a specialization of the MimeMessage class that allows + * you to specify various SMTP options and parameters that will be + * used when this message is sent over SMTP. Simply use this class + * instead of MimeMessage and set SMTP options using the methods on + * this class.

+ * + * See the com.sun.mail.smtp package + * documentation for further information on the SMTP protocol provider.

+ * + * @author Bill Shannon + * @see javax.mail.internet.MimeMessage + */ + +public class SMTPMessage extends MimeMessage { + + /** Never notify of delivery status */ + public static final int NOTIFY_NEVER = -1; + /** Notify of delivery success */ + public static final int NOTIFY_SUCCESS = 1; + /** Notify of delivery failure */ + public static final int NOTIFY_FAILURE = 2; + /** Notify of delivery delay */ + public static final int NOTIFY_DELAY = 4; + + /** Return full message with delivery status notification */ + public static final int RETURN_FULL = 1; + /** Return only message headers with delivery status notification */ + public static final int RETURN_HDRS = 2; + + private static final String[] returnOptionString = { null, "FULL", "HDRS" }; + + private String envelopeFrom; // the string to use in the MAIL FROM: command + private int notifyOptions = 0; + private int returnOption = 0; + private boolean sendPartial = false; + private boolean allow8bitMIME = false; + private String submitter = null; // RFC 2554 AUTH=submitter + private String extension = null; // extensions to use with MAIL command + + /** + * Default constructor. An empty message object is created. + * The headers field is set to an empty InternetHeaders + * object. The flags field is set to an empty Flags + * object. The modified flag is set to true. + * + * @param session the Session + */ + public SMTPMessage(Session session) { + super(session); + } + + /** + * Constructs an SMTPMessage by reading and parsing the data from the + * specified MIME InputStream. The InputStream will be left positioned + * at the end of the data for the message. Note that the input stream + * parse is done within this constructor itself. + * + * @param session Session object for this message + * @param is the message input stream + * @exception MessagingException for failures + */ + public SMTPMessage(Session session, InputStream is) + throws MessagingException { + super(session, is); + } + + /** + * Constructs a new SMTPMessage with content initialized from the + * source MimeMessage. The new message is independent + * of the original.

+ * + * Note: The current implementation is rather inefficient, copying + * the data more times than strictly necessary. + * + * @param source the message to copy content from + * @exception MessagingException for failures + */ + public SMTPMessage(MimeMessage source) throws MessagingException { + super(source); + } + + /** + * Set the From address to appear in the SMTP envelope. Note that this + * is different than the From address that appears in the message itself. + * The envelope From address is typically used when reporting errors. + * See RFC 821 for + * details.

+ * + * If set, overrides the mail.smtp.from property. + * + * @param from the envelope From address + */ + public void setEnvelopeFrom(String from) { + envelopeFrom = from; + } + + /** + * Return the envelope From address. + * + * @return the envelope From address, or null if not set + */ + public String getEnvelopeFrom() { + return envelopeFrom; + } + + /** + * Set notification options to be used if the server supports + * Delivery Status Notification + * (RFC 1891). + * Either NOTIFY_NEVER or some combination of + * NOTIFY_SUCCESS, NOTIFY_FAILURE, and + * NOTIFY_DELAY.

+ * + * If set, overrides the mail.smtp.dsn.notify property. + * + * @param options notification options + */ + public void setNotifyOptions(int options) { + if (options < -1 || options >= 8) + throw new IllegalArgumentException("Bad return option"); + notifyOptions = options; + } + + /** + * Get notification options. Returns zero if no options set. + * + * @return notification options + */ + public int getNotifyOptions() { + return notifyOptions; + } + + /** + * Return notification options as an RFC 1891 string. + * Returns null if no options set. + */ + String getDSNNotify() { + if (notifyOptions == 0) + return null; + if (notifyOptions == NOTIFY_NEVER) + return "NEVER"; + StringBuilder sb = new StringBuilder(); + if ((notifyOptions & NOTIFY_SUCCESS) != 0) + sb.append("SUCCESS"); + if ((notifyOptions & NOTIFY_FAILURE) != 0) { + if (sb.length() != 0) + sb.append(','); + sb.append("FAILURE"); + } + if ((notifyOptions & NOTIFY_DELAY) != 0) { + if (sb.length() != 0) + sb.append(','); + sb.append("DELAY"); + } + return sb.toString(); + } + + /** + * Set return option to be used if server supports + * Delivery Status Notification + * (RFC 1891). + * Either RETURN_FULL or RETURN_HDRS.

+ * + * If set, overrides the mail.smtp.dsn.ret property. + * + * @param option return option + */ + public void setReturnOption(int option) { + if (option < 0 || option > RETURN_HDRS) + throw new IllegalArgumentException("Bad return option"); + returnOption = option; + } + + /** + * Return return option. Returns zero if no option set. + * + * @return return option + */ + public int getReturnOption() { + return returnOption; + } + + /** + * Return return option as an RFC 1891 string. + * Returns null if no option set. + */ + String getDSNRet() { + return returnOptionString[returnOption]; + } + + /** + * If set to true, and the server supports the 8BITMIME extension, text + * parts of this message that use the "quoted-printable" or "base64" + * encodings are converted to use "8bit" encoding if they follow the + * RFC 2045 rules for 8bit text.

+ * + * If true, overrides the mail.smtp.allow8bitmime property. + * + * @param allow allow 8-bit flag + */ + public void setAllow8bitMIME(boolean allow) { + allow8bitMIME = allow; + } + + /** + * Is use of the 8BITMIME extension is allowed? + * + * @return allow 8-bit flag + */ + public boolean getAllow8bitMIME() { + return allow8bitMIME; + } + + /** + * If set to true, and this message has some valid and some invalid + * addresses, send the message anyway, reporting the partial failure with + * a SendFailedException. If set to false (the default), the message is + * not sent to any of the recipients if there is an invalid recipient + * address.

+ * + * If true, overrides the mail.smtp.sendpartial property. + * + * @param partial send partial flag + */ + public void setSendPartial(boolean partial) { + sendPartial = partial; + } + + /** + * Send message if some addresses are invalid? + * + * @return send partial flag + */ + public boolean getSendPartial() { + return sendPartial; + } + + /** + * Gets the submitter to be used for the RFC 2554 AUTH= value + * in the MAIL FROM command. + * + * @return the name of the submitter. + */ + public String getSubmitter() { + return submitter; + } + + /** + * Sets the submitter to be used for the RFC 2554 AUTH= value + * in the MAIL FROM command. Normally only used by a server + * that's relaying a message. Clients will typically not + * set a submitter. See + * RFC 2554 + * for details. + * + * @param submitter the name of the submitter + */ + public void setSubmitter(String submitter) { + this.submitter = submitter; + } + + /** + * Gets the extension string to use with the MAIL command. + * + * @return the extension string + * + * @since JavaMail 1.3.2 + */ + public String getMailExtension() { + return extension; + } + + /** + * Set the extension string to use with the MAIL command. + * The extension string can be used to specify standard SMTP + * service extensions as well as vendor-specific extensions. + * Typically the application should use the + * {@link com.sun.mail.smtp.SMTPTransport SMTPTransport} + * method {@link com.sun.mail.smtp.SMTPTransport#supportsExtension + * supportsExtension} + * to verify that the server supports the desired service extension. + * See RFC 1869 + * and other RFCs that define specific extensions.

+ * + * For example: + * + *

+     * if (smtpTransport.supportsExtension("DELIVERBY"))
+     *    smtpMsg.setMailExtension("BY=60;R");
+     * 
+ * + * @param extension the extension string + * @since JavaMail 1.3.2 + */ + public void setMailExtension(String extension) { + this.extension = extension; + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SMTPOutputStream.java b/app/src/main/java/com/sun/mail/smtp/SMTPOutputStream.java new file mode 100644 index 0000000000..d2fd75b911 --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SMTPOutputStream.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.*; +import com.sun.mail.util.CRLFOutputStream; + +/** + * In addition to converting lines into the canonical format, + * i.e., terminating lines with the CRLF sequence, escapes the "." + * by adding another "." to any "." that appears in the beginning + * of a line. See RFC821 section 4.5.2. + * + * @author Max Spivak + * @see CRLFOutputStream + */ +public class SMTPOutputStream extends CRLFOutputStream { + public SMTPOutputStream(OutputStream os) { + super(os); + } + + @Override + public void write(int b) throws IOException { + // if that last character was a newline, and the current + // character is ".", we always write out an extra ".". + if ((lastb == '\n' || lastb == '\r' || lastb == -1) && b == '.') { + out.write('.'); + } + + super.write(b); + } + + /* + * This method has been added to improve performance. + */ + @Override + public void write(byte b[], int off, int len) throws IOException { + int lastc = (lastb == -1) ? '\n' : lastb; + int start = off; + + len += off; + for (int i = off; i < len; i++) { + if ((lastc == '\n' || lastc == '\r') && b[i] == '.') { + super.write(b, start, i - start); + out.write('.'); + start = i; + } + lastc = b[i]; + } + if ((len - start) > 0) + super.write(b, start, len - start); + } + + /** + * Override flush method in FilterOutputStream. + * + * The MimeMessage writeTo method flushes its buffer at the end, + * but we don't want to flush data out to the socket until we've + * also written the terminating "\r\n.\r\n". + * + * We buffer nothing so there's nothing to flush. We depend + * on the fact that CRLFOutputStream also buffers nothing. + * SMTPTransport will manually flush the socket before reading + * the response. + */ + @Override + public void flush() { + // do nothing + } + + /** + * Ensure we're at the beginning of a line. + * Write CRLF if not. + * + * @exception IOException if the write fails + */ + public void ensureAtBOL() throws IOException { + if (!atBOL) + super.writeln(); + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SMTPProvider.java b/app/src/main/java/com/sun/mail/smtp/SMTPProvider.java new file mode 100644 index 0000000000..71f5703f43 --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SMTPProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.Provider; + +import com.sun.mail.util.DefaultProvider; + +/** + * The SMTP protocol provider. + */ +@DefaultProvider // Remove this annotation if you copy this provider +public class SMTPProvider extends Provider { + public SMTPProvider() { + super(Provider.Type.TRANSPORT, "smtp", SMTPTransport.class.getName(), + "Oracle", null); + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SMTPSSLProvider.java b/app/src/main/java/com/sun/mail/smtp/SMTPSSLProvider.java new file mode 100644 index 0000000000..fe4d09e603 --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SMTPSSLProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.Provider; + +import com.sun.mail.util.DefaultProvider; + +/** + * The SMTP SSL protocol provider. + */ +@DefaultProvider // Remove this annotation if you copy this provider +public class SMTPSSLProvider extends Provider { + public SMTPSSLProvider() { + super(Provider.Type.TRANSPORT, "smtps", + SMTPSSLTransport.class.getName(), "Oracle", null); + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SMTPSSLTransport.java b/app/src/main/java/com/sun/mail/smtp/SMTPSSLTransport.java new file mode 100644 index 0000000000..f3a7300849 --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SMTPSSLTransport.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.*; + +/** + * This class implements the Transport abstract class using SMTP + * over SSL for message submission and transport. + * + * @author Bill Shannon + */ + +public class SMTPSSLTransport extends SMTPTransport { + + /** + * Constructor. + * + * @param session the Session + * @param urlname the URLName of this transport + */ + public SMTPSSLTransport(Session session, URLName urlname) { + super(session, urlname, "smtps", true); + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SMTPSendFailedException.java b/app/src/main/java/com/sun/mail/smtp/SMTPSendFailedException.java new file mode 100644 index 0000000000..d8cf3e66a8 --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SMTPSendFailedException.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.Address; +import javax.mail.SendFailedException; +import javax.mail.internet.InternetAddress; + +/** + * This exception is thrown when the message cannot be sent.

+ * + * This exception will usually appear first in a chained list of exceptions, + * followed by SMTPAddressFailedExceptions and/or + * SMTPAddressSucceededExceptions, * one per address. + * This exception corresponds to one of the SMTP commands used to + * send a message, such as the MAIL, DATA, and "end of data" commands, + * but not including the RCPT command. + * + * @since JavaMail 1.3.2 + */ + +public class SMTPSendFailedException extends SendFailedException { + protected InternetAddress addr; // address that failed + protected String cmd; // command issued to server + protected int rc; // return code from SMTP server + + private static final long serialVersionUID = 8049122628728932894L; + + /** + * Constructs an SMTPSendFailedException with the specified + * address, return code, and error string. + * + * @param cmd the command that was sent to the SMTP server + * @param rc the SMTP return code indicating the failure + * @param err the error string from the SMTP server + * @param ex a chained exception + * @param vs the valid addresses the message was sent to + * @param vus the valid addresses the message was not sent to + * @param inv the invalid addresses + */ + public SMTPSendFailedException(String cmd, int rc, String err, Exception ex, + Address[] vs, Address[] vus, Address[] inv) { + super(err, ex, vs, vus, inv); + this.cmd = cmd; + this.rc = rc; + } + + /** + * Return the command that failed. + * + * @return the command + */ + public String getCommand() { + return cmd; + } + + /** + * Return the return code from the SMTP server that indicates the + * reason for the failure. See + * RFC 821 + * for interpretation of the return code. + * + * @return the return code + */ + public int getReturnCode() { + return rc; + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SMTPSenderFailedException.java b/app/src/main/java/com/sun/mail/smtp/SMTPSenderFailedException.java new file mode 100644 index 0000000000..3139c79ff3 --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SMTPSenderFailedException.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.SendFailedException; +import javax.mail.internet.InternetAddress; + +/** + * This exception is thrown when the message cannot be sent.

+ * + * The exception includes the sender's address, which the mail server + * rejected. + * + * @since JavaMail 1.4.4 + */ + +public class SMTPSenderFailedException extends SendFailedException { + protected InternetAddress addr; // address that failed + protected String cmd; // command issued to server + protected int rc; // return code from SMTP server + + private static final long serialVersionUID = 514540454964476947L; + + /** + * Constructs an SMTPSenderFailedException with the specified + * address, return code, and error string. + * + * @param addr the address that failed + * @param cmd the command that was sent to the SMTP server + * @param rc the SMTP return code indicating the failure + * @param err the error string from the SMTP server + */ + public SMTPSenderFailedException(InternetAddress addr, String cmd, int rc, + String err) { + super(err); + this.addr = addr; + this.cmd = cmd; + this.rc = rc; + } + + /** + * Return the address that failed. + * + * @return the address + */ + public InternetAddress getAddress() { + return addr; + } + + /** + * Return the command that failed. + * + * @return the command + */ + public String getCommand() { + return cmd; + } + + + /** + * Return the return code from the SMTP server that indicates the + * reason for the failure. See + * RFC 821 + * for interpretation of the return code. + * + * @return the return code + */ + public int getReturnCode() { + return rc; + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SMTPTransport.java b/app/src/main/java/com/sun/mail/smtp/SMTPTransport.java new file mode 100644 index 0000000000..efbab723ab --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SMTPTransport.java @@ -0,0 +1,2831 @@ +/* + * Copyright (c) 1997, 2020 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.*; +import java.net.*; +import java.util.*; +import java.util.logging.Level; +import java.lang.reflect.*; +import java.nio.charset.StandardCharsets; +import javax.net.ssl.SSLSocket; + +import javax.mail.*; +import javax.mail.event.*; +import javax.mail.internet.*; + +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.util.SocketFetcher; +import com.sun.mail.util.MailConnectException; +import com.sun.mail.util.SocketConnectException; +import com.sun.mail.util.BASE64EncoderStream; +import com.sun.mail.util.LineInputStream; +import com.sun.mail.util.TraceInputStream; +import com.sun.mail.util.TraceOutputStream; +import com.sun.mail.auth.Ntlm; + +/** + * This class implements the Transport abstract class using SMTP for + * message submission and transport.

+ * + * See the com.sun.mail.smtp package + * documentation for further information on the SMTP protocol provider.

+ * + * This class includes many protected methods that allow a subclass to + * extend this class and add support for non-standard SMTP commands. + * The {@link #issueCommand} and {@link #sendCommand} methods can be + * used to send simple SMTP commands. Other methods such as the + * {@link #mailFrom} and {@link #data} methods can be overridden to + * insert new commands before or after the corresponding SMTP commands. + * For example, a subclass could do this to send the XACT command + * before sending the DATA command: + *

+ *	protected OutputStream data() throws MessagingException {
+ *	    if (supportsExtension("XACCOUNTING"))
+ *	        issueCommand("XACT", 25);
+ *	    return super.data();
+ *	}
+ * 
+ * + * @author Max Spivak + * @author Bill Shannon + * @author Dean Gibson (DIGEST-MD5 authentication) + * @author Lu\u00EDs Serralheiro (NTLM authentication) + * + * @see javax.mail.event.ConnectionEvent + * @see javax.mail.event.TransportEvent + */ + +public class SMTPTransport extends Transport { + + private String name = "smtp"; // Name of this protocol + private int defaultPort = 25; // default SMTP port + private boolean isSSL = false; // use SSL? + private String host; // host we're connected to + + // Following fields valid only during the sendMessage method. + private MimeMessage message; // Message to be sent + private Address[] addresses; // Addresses to which to send the msg + // Valid sent, valid unsent and invalid addresses + private Address[] validSentAddr, validUnsentAddr, invalidAddr; + // Did we send the message even though some addresses were invalid? + private boolean sendPartiallyFailed = false; + // If so, here's an exception we need to throw + private MessagingException exception; + // stream where message data is written + private SMTPOutputStream dataStream; + + // Map of SMTP service extensions supported by server, if EHLO used. + private Hashtable extMap; + + private Map authenticators + = new HashMap<>(); + private String defaultAuthenticationMechanisms; // set in constructor + + private boolean quitWait = false; // true if we should wait + private boolean quitOnSessionReject = false; // true if we should send quit when session initiation is rejected + + private String saslRealm = UNKNOWN; + private String authorizationID = UNKNOWN; + private boolean enableSASL = false; // enable SASL authentication + private boolean useCanonicalHostName = false; // use canonical host name? + private String[] saslMechanisms = UNKNOWN_SA; + + private String ntlmDomain = UNKNOWN; // for ntlm authentication + + private boolean reportSuccess; // throw an exception even on success + private boolean useStartTLS; // use STARTTLS command + private boolean requireStartTLS; // require STARTTLS command + private boolean useRset; // use RSET instead of NOOP + private boolean noopStrict = true; // NOOP must return 250 for success + + private MailLogger logger; // debug logger + private MailLogger traceLogger; // protocol trace logger + private String localHostName; // our own host name + private String lastServerResponse; // last SMTP response + private int lastReturnCode; // last SMTP return code + private boolean notificationDone; // only notify once per send + + private SaslAuthenticator saslAuthenticator; // if SASL is being used + + private boolean noauthdebug = true; // hide auth info in debug output + private boolean debugusername; // include username in debug output? + private boolean debugpassword; // include password in debug output? + private boolean allowutf8; // allow UTF-8 usernames and passwords? + private int chunkSize; // chunk size if CHUNKING supported + + /** Headers that should not be included when sending */ + private static final String[] ignoreList = { "Bcc", "Content-Length" }; + private static final byte[] CRLF = { (byte)'\r', (byte)'\n' }; + private static final String UNKNOWN = "UNKNOWN"; // place holder + private static final String[] UNKNOWN_SA = new String[0]; // place holder + + /** + * Constructor that takes a Session object and a URLName + * that represents a specific SMTP server. + * + * @param session the Session + * @param urlname the URLName of this transport + */ + public SMTPTransport(Session session, URLName urlname) { + this(session, urlname, "smtp", false); + } + + /** + * Constructor used by this class and by SMTPSSLTransport subclass. + * + * @param session the Session + * @param urlname the URLName of this transport + * @param name the protocol name of this transport + * @param isSSL use SSL to connect? + */ + protected SMTPTransport(Session session, URLName urlname, + String name, boolean isSSL) { + super(session, urlname); + Properties props = session.getProperties(); + + logger = new MailLogger(this.getClass(), "DEBUG SMTP", + session.getDebug(), session.getDebugOut()); + traceLogger = logger.getSubLogger("protocol", null); + noauthdebug = !PropUtil.getBooleanProperty(props, + "mail.debug.auth", false); + debugusername = PropUtil.getBooleanProperty(props, + "mail.debug.auth.username", true); + debugpassword = PropUtil.getBooleanProperty(props, + "mail.debug.auth.password", false); + if (urlname != null) + name = urlname.getProtocol(); + this.name = name; + if (!isSSL) + isSSL = PropUtil.getBooleanProperty(props, + "mail." + name + ".ssl.enable", false); + if (isSSL) + this.defaultPort = 465; + else + this.defaultPort = 25; + this.isSSL = isSSL; + + // setting mail.smtp.quitwait to false causes us to not wait for the + // response from the QUIT command + quitWait = PropUtil.getBooleanProperty(props, + "mail." + name + ".quitwait", true); + + // setting mail.smtp.quitonsessionreject to false causes us to directly + // close the socket without sending a QUIT command + quitOnSessionReject = PropUtil.getBooleanProperty(props, + "mail." + name + ".quitonsessionreject", false); + + // mail.smtp.reportsuccess causes us to throw an exception on success + reportSuccess = PropUtil.getBooleanProperty(props, + "mail." + name + ".reportsuccess", false); + + // mail.smtp.starttls.enable enables use of STARTTLS command + useStartTLS = PropUtil.getBooleanProperty(props, + "mail." + name + ".starttls.enable", false); + + // mail.smtp.starttls.required requires use of STARTTLS command + requireStartTLS = PropUtil.getBooleanProperty(props, + "mail." + name + ".starttls.required", false); + + // mail.smtp.userset causes us to use RSET instead of NOOP + // for isConnected + useRset = PropUtil.getBooleanProperty(props, + "mail." + name + ".userset", false); + + // mail.smtp.noop.strict requires 250 response to indicate success + noopStrict = PropUtil.getBooleanProperty(props, + "mail." + name + ".noop.strict", true); + + // check if SASL is enabled + enableSASL = PropUtil.getBooleanProperty(props, + "mail." + name + ".sasl.enable", false); + if (enableSASL) + logger.config("enable SASL"); + useCanonicalHostName = PropUtil.getBooleanProperty(props, + "mail." + name + ".sasl.usecanonicalhostname", false); + if (useCanonicalHostName) + logger.config("use canonical host name"); + + allowutf8 = PropUtil.getBooleanProperty(props, + "mail.mime.allowutf8", false); + if (allowutf8) + logger.config("allow UTF-8"); + + chunkSize = PropUtil.getIntProperty(props, + "mail." + name + ".chunksize", -1); + if (chunkSize > 0 && logger.isLoggable(Level.CONFIG)) + logger.config("chunk size " + chunkSize); + + // created here, because they're inner classes that reference "this" + Authenticator[] a = new Authenticator[] { + new LoginAuthenticator(), + new PlainAuthenticator(), + new DigestMD5Authenticator(), + new NtlmAuthenticator(), + new OAuth2Authenticator() + }; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < a.length; i++) { + authenticators.put(a[i].getMechanism(), a[i]); + sb.append(a[i].getMechanism()).append(' '); + } + defaultAuthenticationMechanisms = sb.toString(); + } + + /** + * Get the name of the local host, for use in the EHLO and HELO commands. + * The property mail.smtp.localhost overrides mail.smtp.localaddress, + * which overrides what InetAddress would tell us. + * + * @return the local host name + */ + public synchronized String getLocalHost() { + // get our hostname and cache it for future use + if (localHostName == null || localHostName.length() <= 0) + localHostName = + session.getProperty("mail." + name + ".localhost"); + if (localHostName == null || localHostName.length() <= 0) + localHostName = + session.getProperty("mail." + name + ".localaddress"); + try { + if (localHostName == null || localHostName.length() <= 0) { + InetAddress localHost = InetAddress.getLocalHost(); + localHostName = localHost.getCanonicalHostName(); + // if we can't get our name, use local address literal + if (localHostName == null) + // XXX - not correct for IPv6 + localHostName = "[" + localHost.getHostAddress() + "]"; + } + } catch (UnknownHostException uhex) { + } + + // last chance, try to get our address from our socket + if (localHostName == null || localHostName.length() <= 0) { + if (serverSocket != null && serverSocket.isBound()) { + InetAddress localHost = serverSocket.getLocalAddress(); + localHostName = localHost.getCanonicalHostName(); + // if we can't get our name, use local address literal + if (localHostName == null) + // XXX - not correct for IPv6 + localHostName = "[" + localHost.getHostAddress() + "]"; + } + } + return localHostName; + } + + /** + * Set the name of the local host, for use in the EHLO and HELO commands. + * + * @param localhost the local host name + * @since JavaMail 1.3.1 + */ + public synchronized void setLocalHost(String localhost) { + localHostName = localhost; + } + + /** + * Start the SMTP protocol on the given socket, which was already + * connected by the caller. Useful for implementing the SMTP ATRN + * command (RFC 2645) where an existing connection is used when + * the server reverses roles and becomes the client. + * + * @param socket the already connected socket + * @exception MessagingException for failures + * @since JavaMail 1.3.3 + */ + public synchronized void connect(Socket socket) throws MessagingException { + serverSocket = socket; + super.connect(); + } + + /** + * Gets the authorization ID to be used for authentication. + * + * @return the authorization ID to use for authentication. + * + * @since JavaMail 1.4.4 + */ + public synchronized String getAuthorizationId() { + if (authorizationID == UNKNOWN) { + authorizationID = + session.getProperty("mail." + name + ".sasl.authorizationid"); + } + return authorizationID; + } + + /** + * Sets the authorization ID to be used for authentication. + * + * @param authzid the authorization ID to use for + * authentication. + * + * @since JavaMail 1.4.4 + */ + public synchronized void setAuthorizationID(String authzid) { + this.authorizationID = authzid; + } + + /** + * Is SASL authentication enabled? + * + * @return true if SASL authentication is enabled + * + * @since JavaMail 1.4.4 + */ + public synchronized boolean getSASLEnabled() { + return enableSASL; + } + + /** + * Set whether SASL authentication is enabled. + * + * @param enableSASL should we enable SASL authentication? + * + * @since JavaMail 1.4.4 + */ + public synchronized void setSASLEnabled(boolean enableSASL) { + this.enableSASL = enableSASL; + } + + /** + * Gets the SASL realm to be used for DIGEST-MD5 authentication. + * + * @return the name of the realm to use for SASL authentication. + * + * @since JavaMail 1.3.1 + */ + public synchronized String getSASLRealm() { + if (saslRealm == UNKNOWN) { + saslRealm = session.getProperty("mail." + name + ".sasl.realm"); + if (saslRealm == null) // try old name + saslRealm = session.getProperty("mail." + name + ".saslrealm"); + } + return saslRealm; + } + + /** + * Sets the SASL realm to be used for DIGEST-MD5 authentication. + * + * @param saslRealm the name of the realm to use for + * SASL authentication. + * + * @since JavaMail 1.3.1 + */ + public synchronized void setSASLRealm(String saslRealm) { + this.saslRealm = saslRealm; + } + + /** + * Should SASL use the canonical host name? + * + * @return true if SASL should use the canonical host name + * + * @since JavaMail 1.5.2 + */ + public synchronized boolean getUseCanonicalHostName() { + return useCanonicalHostName; + } + + /** + * Set whether SASL should use the canonical host name. + * + * @param useCanonicalHostName should SASL use the canonical host name? + * + * @since JavaMail 1.5.2 + */ + public synchronized void setUseCanonicalHostName( + boolean useCanonicalHostName) { + this.useCanonicalHostName = useCanonicalHostName; + } + + /** + * Get the list of SASL mechanisms to consider if SASL authentication + * is enabled. If the list is empty or null, all available SASL mechanisms + * are considered. + * + * @return the array of SASL mechanisms to consider + * + * @since JavaMail 1.4.4 + */ + public synchronized String[] getSASLMechanisms() { + if (saslMechanisms == UNKNOWN_SA) { + List v = new ArrayList<>(5); + String s = session.getProperty("mail." + name + ".sasl.mechanisms"); + if (s != null && s.length() > 0) { + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL mechanisms allowed: " + s); + StringTokenizer st = new StringTokenizer(s, " ,"); + while (st.hasMoreTokens()) { + String m = st.nextToken(); + if (m.length() > 0) + v.add(m); + } + } + saslMechanisms = new String[v.size()]; + v.toArray(saslMechanisms); + } + if (saslMechanisms == null) + return null; + return saslMechanisms.clone(); + } + + /** + * Set the list of SASL mechanisms to consider if SASL authentication + * is enabled. If the list is empty or null, all available SASL mechanisms + * are considered. + * + * @param mechanisms the array of SASL mechanisms to consider + * + * @since JavaMail 1.4.4 + */ + public synchronized void setSASLMechanisms(String[] mechanisms) { + if (mechanisms != null) + mechanisms = mechanisms.clone(); + this.saslMechanisms = mechanisms; + } + + /** + * Gets the NTLM domain to be used for NTLM authentication. + * + * @return the name of the domain to use for NTLM authentication. + * + * @since JavaMail 1.4.3 + */ + public synchronized String getNTLMDomain() { + if (ntlmDomain == UNKNOWN) { + ntlmDomain = + session.getProperty("mail." + name + ".auth.ntlm.domain"); + } + return ntlmDomain; + } + + /** + * Sets the NTLM domain to be used for NTLM authentication. + * + * @param ntlmDomain the name of the domain to use for + * NTLM authentication. + * + * @since JavaMail 1.4.3 + */ + public synchronized void setNTLMDomain(String ntlmDomain) { + this.ntlmDomain = ntlmDomain; + } + + /** + * Should we report even successful sends by throwing an exception? + * If so, a SendFailedException will always be thrown and + * an {@link com.sun.mail.smtp.SMTPAddressSucceededException + * SMTPAddressSucceededException} will be included in the exception + * chain for each successful address, along with the usual + * {@link com.sun.mail.smtp.SMTPAddressFailedException + * SMTPAddressFailedException} for each unsuccessful address. + * + * @return true if an exception will be thrown on successful sends. + * + * @since JavaMail 1.3.2 + */ + public synchronized boolean getReportSuccess() { + return reportSuccess; + } + + /** + * Set whether successful sends should be reported by throwing + * an exception. + * + * @param reportSuccess should we throw an exception on success? + * + * @since JavaMail 1.3.2 + */ + public synchronized void setReportSuccess(boolean reportSuccess) { + this.reportSuccess = reportSuccess; + } + + /** + * Should we use the STARTTLS command to secure the connection + * if the server supports it? + * + * @return true if the STARTTLS command will be used + * + * @since JavaMail 1.3.2 + */ + public synchronized boolean getStartTLS() { + return useStartTLS; + } + + /** + * Set whether the STARTTLS command should be used. + * + * @param useStartTLS should we use the STARTTLS command? + * + * @since JavaMail 1.3.2 + */ + public synchronized void setStartTLS(boolean useStartTLS) { + this.useStartTLS = useStartTLS; + } + + /** + * Should we require the STARTTLS command to secure the connection? + * + * @return true if the STARTTLS command will be required + * + * @since JavaMail 1.4.2 + */ + public synchronized boolean getRequireStartTLS() { + return requireStartTLS; + } + + /** + * Set whether the STARTTLS command should be required. + * + * @param requireStartTLS should we require the STARTTLS command? + * + * @since JavaMail 1.4.2 + */ + public synchronized void setRequireStartTLS(boolean requireStartTLS) { + this.requireStartTLS = requireStartTLS; + } + + /** + * Is this Transport using SSL to connect to the server? + * + * @return true if using SSL + * @since JavaMail 1.4.6 + */ + public synchronized boolean isSSL() { + return serverSocket instanceof SSLSocket; + } + + /** + * Should we use the RSET command instead of the NOOP command + * in the @{link #isConnected isConnected} method? + * + * @return true if RSET will be used + * + * @since JavaMail 1.4 + */ + public synchronized boolean getUseRset() { + return useRset; + } + + /** + * Set whether the RSET command should be used instead of the + * NOOP command in the @{link #isConnected isConnected} method. + * + * @param useRset should we use the RSET command? + * + * @since JavaMail 1.4 + */ + public synchronized void setUseRset(boolean useRset) { + this.useRset = useRset; + } + + /** + * Is the NOOP command required to return a response code + * of 250 to indicate success? + * + * @return true if NOOP must return 250 + * + * @since JavaMail 1.4.3 + */ + public synchronized boolean getNoopStrict() { + return noopStrict; + } + + /** + * Set whether the NOOP command is required to return a response code + * of 250 to indicate success. + * + * @param noopStrict is NOOP required to return 250? + * + * @since JavaMail 1.4.3 + */ + public synchronized void setNoopStrict(boolean noopStrict) { + this.noopStrict = noopStrict; + } + + /** + * Return the last response we got from the server. + * A failed send is often followed by an RSET command, + * but the response from the RSET command is not saved. + * Instead, this returns the response from the command + * before the RSET command. + * + * @return last response from server + * + * @since JavaMail 1.3.2 + */ + public synchronized String getLastServerResponse() { + return lastServerResponse; + } + + /** + * Return the return code from the last response we got from the server. + * + * @return return code from last response from server + * + * @since JavaMail 1.4.1 + */ + public synchronized int getLastReturnCode() { + return lastReturnCode; + } + + /** + * Performs the actual protocol-specific connection attempt. + * Will attempt to connect to "localhost" if the host was null.

+ * + * Unless mail.smtp.ehlo is set to false, we'll try to identify + * ourselves using the ESMTP command EHLO. + * + * If mail.smtp.auth is set to true, we insist on having a username + * and password, and will try to authenticate ourselves if the server + * supports the AUTH extension (RFC 2554). + * + * @param host the name of the host to connect to + * @param port the port to use (-1 means use default port) + * @param user the name of the user to login as + * @param password the user's password + * @return true if connection successful, false if authentication failed + * @exception MessagingException for non-authentication failures + */ + @Override + protected synchronized boolean protocolConnect(String host, int port, + String user, String password) + throws MessagingException { + Properties props = session.getProperties(); + + // setting mail.smtp.auth to true enables attempts to use AUTH + boolean useAuth = PropUtil.getBooleanProperty(props, + "mail." + name + ".auth", false); + + /* + * If mail.smtp.auth is set, make sure we have a valid username + * and password, even if we might not end up using it (e.g., + * because the server doesn't support ESMTP or doesn't support + * the AUTH extension). + */ + if (useAuth && (user == null || password == null)) { + if (logger.isLoggable(Level.FINE)) { + logger.fine("need username and password for authentication"); + logger.fine("protocolConnect returning false" + + ", host=" + host + + ", user=" + traceUser(user) + + ", password=" + tracePassword(password)); + } + return false; + } + + // setting mail.smtp.ehlo to false disables attempts to use EHLO + boolean useEhlo = PropUtil.getBooleanProperty(props, + "mail." + name + ".ehlo", true); + if (logger.isLoggable(Level.FINE)) + logger.fine("useEhlo " + useEhlo + ", useAuth " + useAuth); + + /* + * If port is not specified, set it to value of mail.smtp.port + * property if it exists, otherwise default to 25. + */ + if (port == -1) + port = PropUtil.getIntProperty(props, + "mail." + name + ".port", -1); + if (port == -1) + port = defaultPort; + + if (host == null || host.length() == 0) + host = "localhost"; + + /* + * If anything goes wrong, we need to be sure + * to close the connection. + */ + boolean connected = false; + try { + + if (serverSocket != null) + openServer(); // only happens from connect(socket) + else + openServer(host, port); + + boolean succeed = false; + if (useEhlo) + succeed = ehlo(getLocalHost()); + if (!succeed) + helo(getLocalHost()); + + if (useStartTLS || requireStartTLS) { + if (serverSocket instanceof SSLSocket) { + logger.fine("STARTTLS requested but already using SSL"); + } else if (supportsExtension("STARTTLS")) { + startTLS(); + /* + * Have to issue another EHLO to update list of extensions + * supported, especially authentication mechanisms. + * Don't know if this could ever fail, but we ignore + * failure. + */ + ehlo(getLocalHost()); + } else if (requireStartTLS) { + logger.fine("STARTTLS required but not supported"); + throw new MessagingException( + "STARTTLS is required but " + + "host does not support STARTTLS"); + } + } + + if (allowutf8 && !supportsExtension("SMTPUTF8")) + logger.log(Level.INFO, "mail.mime.allowutf8 set " + + "but server doesn't advertise SMTPUTF8 support"); + + if ((useAuth || (user != null && password != null)) && + (supportsExtension("AUTH") || + supportsExtension("AUTH=LOGIN"))) { + if (logger.isLoggable(Level.FINE)) + logger.fine("protocolConnect login" + + ", host=" + host + + ", user=" + traceUser(user) + + ", password=" + tracePassword(password)); + connected = authenticate(user, password); + return connected; + } + + // we connected correctly + connected = true; + return true; + + } finally { + // if we didn't connect successfully, + // make sure the connection is closed + if (!connected) { + try { + closeConnection(); + } catch (MessagingException mex) { + // ignore it + } + } + } + } + + /** + * Authenticate to the server. + */ + private boolean authenticate(String user, String passwd) + throws MessagingException { + // setting mail.smtp.auth.mechanisms controls which mechanisms will + // be used, and in what order they'll be considered. only the first + // match is used. + String mechs = session.getProperty("mail." + name + ".auth.mechanisms"); + if (mechs == null) + mechs = defaultAuthenticationMechanisms; + + String authzid = getAuthorizationId(); + if (authzid == null) + authzid = user; + if (enableSASL) { + logger.fine("Authenticate with SASL"); + try { + if (sasllogin(getSASLMechanisms(), getSASLRealm(), authzid, + user, passwd)) { + return true; // success + } else { + logger.fine("SASL authentication failed"); + return false; + } + } catch (UnsupportedOperationException ex) { + logger.log(Level.FINE, "SASL support failed", ex); + // if the SASL support fails, fall back to non-SASL + } + } + + if (logger.isLoggable(Level.FINE)) + logger.fine("Attempt to authenticate using mechanisms: " + mechs); + + /* + * Loop through the list of mechanisms supplied by the user + * (or defaulted) and try each in turn. If the server supports + * the mechanism and we have an authenticator for the mechanism, + * and it hasn't been disabled, use it. + */ + StringTokenizer st = new StringTokenizer(mechs); + while (st.hasMoreTokens()) { + String m = st.nextToken(); + m = m.toUpperCase(Locale.ENGLISH); + Authenticator a = authenticators.get(m); + if (a == null) { + logger.log(Level.FINE, "no authenticator for mechanism {0}", m); + continue; + } + + if (!supportsAuthentication(m)) { + logger.log(Level.FINE, "mechanism {0} not supported by server", + m); + continue; + } + + /* + * If using the default mechanisms, check if this one is disabled. + */ + if (mechs == defaultAuthenticationMechanisms) { + String dprop = "mail." + name + ".auth." + + m.toLowerCase(Locale.ENGLISH) + ".disable"; + boolean disabled = PropUtil.getBooleanProperty( + session.getProperties(), + dprop, !a.enabled()); + if (disabled) { + if (logger.isLoggable(Level.FINE)) + logger.fine("mechanism " + m + + " disabled by property: " + dprop); + continue; + } + } + + // only the first supported and enabled mechanism is used + logger.log(Level.FINE, "Using mechanism {0}", m); + return a.authenticate(host, authzid, user, passwd); + } + + // if no authentication mechanism found, fail + throw new AuthenticationFailedException( + "No authentication mechanisms supported by both server and client"); + } + + /** + * Abstract base class for SMTP authentication mechanism implementations. + */ + private abstract class Authenticator { + protected int resp; // the response code, used by subclasses + private final String mech; // the mechanism name, set in the constructor + private final boolean enabled; // is this mechanism enabled by default? + + Authenticator(String mech) { + this(mech, true); + } + + Authenticator(String mech, boolean enabled) { + this.mech = mech.toUpperCase(Locale.ENGLISH); + this.enabled = enabled; + } + + String getMechanism() { + return mech; + } + + boolean enabled() { + return enabled; + } + + /** + * Start the authentication handshake by issuing the AUTH command. + * Delegate to the doAuth method to do the mechanism-specific + * part of the handshake. + */ + boolean authenticate(String host, String authzid, + String user, String passwd) throws MessagingException { + Throwable thrown = null; + try { + // use "initial response" capability, if supported + String ir = getInitialResponse(host, authzid, user, passwd); + if (noauthdebug && isTracing()) { + logger.fine("AUTH " + mech + " command trace suppressed"); + suspendTracing(); + } + if (ir != null) + resp = simpleCommand("AUTH " + mech + " " + + (ir.length() == 0 ? "=" : ir)); + else + resp = simpleCommand("AUTH " + mech); + + /* + * A 530 response indicates that the server wants us to + * issue a STARTTLS command first. Do that and try again. + */ + if (resp == 530) { + startTLS(); + if (ir != null) + resp = simpleCommand("AUTH " + mech + " " + ir); + else + resp = simpleCommand("AUTH " + mech); + } + if (resp == 334) + doAuth(host, authzid, user, passwd); + } catch (IOException ex) { // should never happen, ignore + logger.log(Level.FINE, "AUTH " + mech + " failed", ex); + } catch (Throwable t) { // crypto can't be initialized? + logger.log(Level.FINE, "AUTH " + mech + " failed", t); + thrown = t; + } finally { + if (noauthdebug && isTracing()) + logger.fine("AUTH " + mech + " " + + (resp == 235 ? "succeeded" : "failed")); + resumeTracing(); + if (resp != 235) { + closeConnection(); + if (thrown != null) { + if (thrown instanceof Error) + throw (Error)thrown; + if (thrown instanceof Exception) + throw new AuthenticationFailedException( + getLastServerResponse(), + (Exception)thrown); + assert false : "unknown Throwable"; // can't happen + } + throw new AuthenticationFailedException( + getLastServerResponse()); + } + } + return true; + } + + /** + * Provide the initial response to use in the AUTH command, + * or null if not supported. Subclasses that support the + * initial response capability will override this method. + */ + String getInitialResponse(String host, String authzid, String user, + String passwd) throws MessagingException, IOException { + return null; + } + + abstract void doAuth(String host, String authzid, String user, + String passwd) throws MessagingException, IOException; + } + + /** + * Perform the authentication handshake for LOGIN authentication. + */ + private class LoginAuthenticator extends Authenticator { + LoginAuthenticator() { + super("LOGIN"); + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws MessagingException, IOException { + // send username + resp = simpleCommand(BASE64EncoderStream.encode( + user.getBytes(StandardCharsets.UTF_8))); + if (resp == 334) { + // send passwd + resp = simpleCommand(BASE64EncoderStream.encode( + passwd.getBytes(StandardCharsets.UTF_8))); + } + } + } + + /** + * Perform the authentication handshake for PLAIN authentication. + */ + private class PlainAuthenticator extends Authenticator { + PlainAuthenticator() { + super("PLAIN"); + } + + @Override + String getInitialResponse(String host, String authzid, String user, + String passwd) throws MessagingException, IOException { + // return "authziduserpasswd" + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + OutputStream b64os = + new BASE64EncoderStream(bos, Integer.MAX_VALUE); + if (authzid != null) + b64os.write(authzid.getBytes(StandardCharsets.UTF_8)); + b64os.write(0); + b64os.write(user.getBytes(StandardCharsets.UTF_8)); + b64os.write(0); + b64os.write(passwd.getBytes(StandardCharsets.UTF_8)); + b64os.flush(); // complete the encoding + + return ASCIIUtility.toString(bos.toByteArray()); + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws MessagingException, IOException { + // should never get here + throw new AuthenticationFailedException("PLAIN asked for more"); + } + } + + /** + * Perform the authentication handshake for DIGEST-MD5 authentication. + */ + private class DigestMD5Authenticator extends Authenticator { + private DigestMD5 md5support; // only create if needed + + DigestMD5Authenticator() { + super("DIGEST-MD5"); + } + + private synchronized DigestMD5 getMD5() { + if (md5support == null) + md5support = new DigestMD5(logger); + return md5support; + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws MessagingException, IOException { + DigestMD5 md5 = getMD5(); + assert md5 != null; + + byte[] b = md5.authClient(host, user, passwd, getSASLRealm(), + getLastServerResponse()); + resp = simpleCommand(b); + if (resp == 334) { // client authenticated by server + if (!md5.authServer(getLastServerResponse())) { + // server NOT authenticated by client !!! + resp = -1; + } else { + // send null response + resp = simpleCommand(new byte[0]); + } + } + } + } + + /** + * Perform the authentication handshake for NTLM authentication. + */ + private class NtlmAuthenticator extends Authenticator { + private Ntlm ntlm; + + NtlmAuthenticator() { + super("NTLM"); + } + + @Override + String getInitialResponse(String host, String authzid, String user, + String passwd) throws MessagingException, IOException { + ntlm = new Ntlm(getNTLMDomain(), getLocalHost(), + user, passwd, logger); + + int flags = PropUtil.getIntProperty( + session.getProperties(), + "mail." + name + ".auth.ntlm.flags", 0); + boolean v2 = PropUtil.getBooleanProperty( + session.getProperties(), + "mail." + name + ".auth.ntlm.v2", true); + + String type1 = ntlm.generateType1Msg(flags, v2); + return type1; + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws MessagingException, IOException { + assert ntlm != null; + String type3 = ntlm.generateType3Msg( + getLastServerResponse().substring(4).trim()); + + resp = simpleCommand(type3); + } + } + + /** + * Perform the authentication handshake for XOAUTH2 authentication. + */ + private class OAuth2Authenticator extends Authenticator { + + OAuth2Authenticator() { + super("XOAUTH2", false); // disabled by default + } + + @Override + String getInitialResponse(String host, String authzid, String user, + String passwd) throws MessagingException, IOException { + String resp = "user=" + user + "\001auth=Bearer " + + passwd + "\001\001"; + byte[] b = BASE64EncoderStream.encode( + resp.getBytes(StandardCharsets.UTF_8)); + return ASCIIUtility.toString(b); + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws MessagingException, IOException { + // should never get here + throw new AuthenticationFailedException("OAUTH2 asked for more"); + } + } + + /** + * SASL-based login. + * + * @param allowed the allowed SASL mechanisms + * @param realm the SASL realm + * @param authzid the authorization ID + * @param u the user name for authentication + * @param p the password for authentication + * @return true for success + * @exception MessagingException for failures + */ + private boolean sasllogin(String[] allowed, String realm, String authzid, + String u, String p) throws MessagingException { + String serviceHost; + if (useCanonicalHostName) + serviceHost = serverSocket.getInetAddress().getCanonicalHostName(); + else + serviceHost = host; + if (saslAuthenticator == null) { + try { + Class sac = Class.forName( + "com.sun.mail.smtp.SMTPSaslAuthenticator"); + Constructor c = sac.getConstructor(new Class[] { + SMTPTransport.class, + String.class, + Properties.class, + MailLogger.class, + String.class + }); + saslAuthenticator = (SaslAuthenticator)c.newInstance( + new Object[] { + this, + name, + session.getProperties(), + logger, + serviceHost + }); + } catch (Exception ex) { + logger.log(Level.FINE, "Can't load SASL authenticator", ex); + // probably because we're running on a system without SASL + return false; // not authenticated, try without SASL + } + } + + // were any allowed mechanisms specified? + List v; + if (allowed != null && allowed.length > 0) { + // remove anything not supported by the server + v = new ArrayList<>(allowed.length); + for (int i = 0; i < allowed.length; i++) + if (supportsAuthentication(allowed[i])) // XXX - case must match + v.add(allowed[i]); + } else { + // everything is allowed + v = new ArrayList<>(); + if (extMap != null) { + String a = extMap.get("AUTH"); + if (a != null) { + StringTokenizer st = new StringTokenizer(a); + while (st.hasMoreTokens()) + v.add(st.nextToken()); + } + } + } + String[] mechs = v.toArray(new String[v.size()]); + try { + if (noauthdebug && isTracing()) { + logger.fine("SASL AUTH command trace suppressed"); + suspendTracing(); + } + return saslAuthenticator.authenticate(mechs, realm, authzid, u, p); + } finally { + resumeTracing(); + } + } + + /** + * Send the Message to the specified list of addresses.

+ * + * If all the addresses succeed the SMTP check + * using the RCPT TO: command, we attempt to send the message. + * A TransportEvent of type MESSAGE_DELIVERED is fired indicating the + * successful submission of a message to the SMTP host.

+ * + * If some of the addresses fail the SMTP check, + * and the mail.smtp.sendpartial property is not set, + * sending is aborted. The TransportEvent of type MESSAGE_NOT_DELIVERED + * is fired containing the valid and invalid addresses. The + * SendFailedException is also thrown.

+ * + * If some of the addresses fail the SMTP check, + * and the mail.smtp.sendpartial property is set to true, + * the message is sent. The TransportEvent of type + * MESSAGE_PARTIALLY_DELIVERED + * is fired containing the valid and invalid addresses. The + * SMTPSendFailedException is also thrown.

+ * + * MessagingException is thrown if the message can't write out + * an RFC822-compliant stream using its writeTo method.

+ * + * @param message The MimeMessage to be sent + * @param addresses List of addresses to send this message to + * @see javax.mail.event.TransportEvent + * @exception SMTPSendFailedException if the send failed because of + * an SMTP command error + * @exception SendFailedException if the send failed because of + * invalid addresses. + * @exception MessagingException if the connection is dead + * or not in the connected state or if the message is + * not a MimeMessage. + */ + @Override + public synchronized void sendMessage(Message message, Address[] addresses) + throws MessagingException, SendFailedException { + + sendMessageStart(message != null ? message.getSubject() : ""); + checkConnected(); + + // check if the message is a valid MIME/RFC822 message and that + // it has all valid InternetAddresses; fail if not + if (!(message instanceof MimeMessage)) { + logger.fine("Can only send RFC822 msgs"); + throw new MessagingException("SMTP can only send RFC822 messages"); + } + if (addresses == null || addresses.length == 0) { + throw new SendFailedException("No recipient addresses"); + } + for (int i = 0; i < addresses.length; i++) { + if (!(addresses[i] instanceof InternetAddress)) { + throw new MessagingException(addresses[i] + + " is not an InternetAddress"); + } + } + + this.message = (MimeMessage)message; + this.addresses = addresses; + validUnsentAddr = addresses; // until we know better + expandGroups(); + + boolean use8bit = false; + if (message instanceof SMTPMessage) + use8bit = ((SMTPMessage)message).getAllow8bitMIME(); + if (!use8bit) + use8bit = PropUtil.getBooleanProperty(session.getProperties(), + "mail." + name + ".allow8bitmime", false); + if (logger.isLoggable(Level.FINE)) + logger.fine("use8bit " + use8bit); + if (use8bit && supportsExtension("8BITMIME")) { + if (convertTo8Bit(this.message)) { + // in case we made any changes, save those changes + // XXX - this will change the Message-ID + try { + this.message.saveChanges(); + } catch (MessagingException mex) { + // ignore it + } + } + } + + try { + mailFrom(); + rcptTo(); + if (chunkSize > 0 && supportsExtension("CHUNKING")) { + /* + * Use BDAT to send the data in chunks. + * Note that even though the BDAT command is able to send + * messages that contain binary data, we can't use it to + * do that because a) we still need to canonicalize the + * line terminators for text data, which we can't tell apart + * from the message content, and b) the message content is + * encoded before we even know that we can use BDAT. + */ + this.message.writeTo(bdat(), ignoreList); + finishBdat(); + } else { + this.message.writeTo(data(), ignoreList); + finishData(); + } + if (sendPartiallyFailed) { + // throw the exception, + // fire TransportEvent.MESSAGE_PARTIALLY_DELIVERED event + logger.fine("Sending partially failed " + + "because of invalid destination addresses"); + notifyTransportListeners( + TransportEvent.MESSAGE_PARTIALLY_DELIVERED, + validSentAddr, validUnsentAddr, invalidAddr, + this.message); + + throw new SMTPSendFailedException(".", lastReturnCode, + lastServerResponse, exception, + validSentAddr, validUnsentAddr, invalidAddr); + } + logger.fine("message successfully delivered to mail server"); + notifyTransportListeners(TransportEvent.MESSAGE_DELIVERED, + validSentAddr, validUnsentAddr, + invalidAddr, this.message); + } catch (MessagingException mex) { + logger.log(Level.FINE, "MessagingException while sending", mex); + // the MessagingException might be wrapping an IOException + if (mex.getNextException() instanceof IOException) { + // if we catch an IOException, it means that we want + // to drop the connection so that the message isn't sent + logger.fine("nested IOException, closing"); + try { + closeConnection(); + } catch (MessagingException cex) { /* ignore it */ } + } + addressesFailed(); + notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, + validSentAddr, validUnsentAddr, + invalidAddr, this.message); + + throw mex; + } catch (IOException ex) { + logger.log(Level.FINE, "IOException while sending, closing", ex); + // if we catch an IOException, it means that we want + // to drop the connection so that the message isn't sent + try { + closeConnection(); + } catch (MessagingException mex) { /* ignore it */ } + addressesFailed(); + notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, + validSentAddr, validUnsentAddr, + invalidAddr, this.message); + + throw new MessagingException("IOException while sending message", + ex); + } finally { + // no reason to keep this data around + validSentAddr = validUnsentAddr = invalidAddr = null; + this.addresses = null; + this.message = null; + this.exception = null; + sendPartiallyFailed = false; + notificationDone = false; // reset for next send + } + sendMessageEnd(); + } + + /** + * The send failed, fix the address arrays to report the failure correctly. + */ + private void addressesFailed() { + if (validSentAddr != null) { + if (validUnsentAddr != null) { + Address newa[] = + new Address[validSentAddr.length + validUnsentAddr.length]; + System.arraycopy(validSentAddr, 0, + newa, 0, validSentAddr.length); + System.arraycopy(validUnsentAddr, 0, + newa, validSentAddr.length, validUnsentAddr.length); + validSentAddr = null; + validUnsentAddr = newa; + } else { + validUnsentAddr = validSentAddr; + validSentAddr = null; + } + } + } + + /** + * Close the Transport and terminate the connection to the server. + */ + @Override + public synchronized void close() throws MessagingException { + if (!super.isConnected()) // Already closed. + return; + try { + if (serverSocket != null) { + sendCommand("QUIT"); + if (quitWait) { + int resp = readServerResponse(); + if (resp != 221 && resp != -1 && + logger.isLoggable(Level.FINE)) + logger.fine("QUIT failed with " + resp); + } + } + } finally { + closeConnection(); + } + } + + private void closeConnection() throws MessagingException { + try { + if (serverSocket != null) + serverSocket.close(); + } catch (IOException ioex) { // shouldn't happen + throw new MessagingException("Server Close Failed", ioex); + } finally { + serverSocket = null; + serverOutput = null; + serverInput = null; + lineInputStream = null; + if (super.isConnected()) // only notify if already connected + super.close(); + } + } + + /** + * Check whether the transport is connected. Override superclass + * method, to actually ping our server connection. + */ + @Override + public synchronized boolean isConnected() { + if (!super.isConnected()) + // if we haven't been connected at all, don't bother with NOOP + return false; + + try { + // sendmail may respond slowly to NOOP after many requests + // so if mail.smtp.userset is set we use RSET instead of NOOP. + if (useRset) + sendCommand("RSET"); + else + sendCommand("NOOP"); + int resp = readServerResponse(); + + /* + * NOOP should return 250 on success, however, SIMS 3.2 returns + * 200, so we work around it. + * + * Hotmail didn't used to implement the NOOP command at all so + * assume any kind of response means we're still connected. + * That is, any response except 421, which means the server + * is shutting down the connection. + * + * Some versions of Exchange return 451 instead of 421 when + * timing out a connection. + * + * Argh! + * + * If mail.smtp.noop.strict is set to false, be tolerant of + * servers that return the wrong response code for success. + */ + if (resp >= 0 && (noopStrict ? resp == 250 : resp != 421)) { + return true; + } else { + try { + closeConnection(); + } catch (MessagingException mex) { + // ignore it + } + return false; + } + } catch (Exception ex) { + try { + closeConnection(); + } catch (MessagingException mex) { + // ignore it + } + return false; + } + } + + /** + * Notify all TransportListeners. Keep track of whether notification + * has been done so as to only notify once per send. + * + * @since JavaMail 1.4.2 + */ + @Override + protected void notifyTransportListeners(int type, Address[] validSent, + Address[] validUnsent, + Address[] invalid, Message msg) { + + if (!notificationDone) { + super.notifyTransportListeners(type, validSent, validUnsent, + invalid, msg); + notificationDone = true; + } + } + + /** + * Expand any group addresses. + */ + private void expandGroups() { + List

groups = null; + for (int i = 0; i < addresses.length; i++) { + InternetAddress a = (InternetAddress)addresses[i]; + if (a.isGroup()) { + if (groups == null) { + // first group, catch up with where we are + groups = new ArrayList<>(); + for (int k = 0; k < i; k++) + groups.add(addresses[k]); + } + // parse it and add each individual address + try { + InternetAddress[] ia = a.getGroup(true); + if (ia != null) { + for (int j = 0; j < ia.length; j++) + groups.add(ia[j]); + } else + groups.add(a); + } catch (ParseException pex) { + // parse failed, add the whole thing + groups.add(a); + } + } else { + // if we've started accumulating a list, add this to it + if (groups != null) + groups.add(a); + } + } + + // if we have a new list, convert it back to an array + if (groups != null) { + InternetAddress[] newa = new InternetAddress[groups.size()]; + groups.toArray(newa); + addresses = newa; + } + } + + /** + * If the Part is a text part and has a Content-Transfer-Encoding + * of "quoted-printable" or "base64", and it obeys the rules for + * "8bit" encoding, change the encoding to "8bit". If the part is + * a multipart, recursively process all its parts. + * + * @return true if any changes were made + * + * XXX - This is really quite a hack. + */ + private boolean convertTo8Bit(MimePart part) { + boolean changed = false; + try { + if (part.isMimeType("text/*")) { + String enc = part.getEncoding(); + if (enc != null && (enc.equalsIgnoreCase("quoted-printable") || + enc.equalsIgnoreCase("base64"))) { + InputStream is = null; + try { + is = part.getInputStream(); + if (is8Bit(is)) { + /* + * If the message was created using an InputStream + * then we have to extract the content as an object + * and set it back as an object so that the content + * will be re-encoded. + * + * If the message was not created using an + * InputStream, the following should have no effect. + */ + part.setContent(part.getContent(), + part.getContentType()); + part.setHeader("Content-Transfer-Encoding", "8bit"); + changed = true; + } + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException ex2) { + // ignore it + } + } + } + } + } else if (part.isMimeType("multipart/*")) { + MimeMultipart mp = (MimeMultipart)part.getContent(); + int count = mp.getCount(); + for (int i = 0; i < count; i++) { + if (convertTo8Bit((MimePart)mp.getBodyPart(i))) + changed = true; + } + } + } catch (IOException ioex) { + // any exception causes us to give up + } catch (MessagingException mex) { + // any exception causes us to give up + } + return changed; + } + + /** + * Check whether the data in the given InputStream follows the + * rules for 8bit text. Lines have to be 998 characters or less + * and no NULs are allowed. CR and LF must occur in pairs but we + * don't check that because we assume this is text and we convert + * all CR/LF combinations into canonical CRLF later. + */ + private boolean is8Bit(InputStream is) { + int b; + int linelen = 0; + boolean need8bit = false; + try { + while ((b = is.read()) >= 0) { + b &= 0xff; + if (b == '\r' || b == '\n') + linelen = 0; + else if (b == 0) + return false; + else { + linelen++; + if (linelen > 998) // 1000 - CRLF + return false; + } + if (b > 0x7f) + need8bit = true; + } + } catch (IOException ex) { + return false; + } + if (need8bit) + logger.fine("found an 8bit part"); + return need8bit; + } + + @Override + protected void finalize() throws Throwable { + try { + closeConnection(); + } catch (MessagingException mex) { + // ignore it + } finally { + super.finalize(); + } + } + + ///////////////////// smtp stuff /////////////////////// + private BufferedInputStream serverInput; + private LineInputStream lineInputStream; + private OutputStream serverOutput; + private Socket serverSocket; + private TraceInputStream traceInput; + private TraceOutputStream traceOutput; + + /////// smtp protocol ////// + + /** + * Issue the HELO command. + * + * @param domain our domain + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected void helo(String domain) throws MessagingException { + if (domain != null) + issueCommand("HELO " + domain, 250); + else + issueCommand("HELO", 250); + } + + /** + * Issue the EHLO command. + * Collect the returned list of service extensions. + * + * @param domain our domain + * @return true if command succeeds + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected boolean ehlo(String domain) throws MessagingException { + String cmd; + if (domain != null) + cmd = "EHLO " + domain; + else + cmd = "EHLO"; + sendCommand(cmd); + int resp = readServerResponse(); + if (resp == 250) { + // extract the supported service extensions + BufferedReader rd = + new BufferedReader(new StringReader(lastServerResponse)); + String line; + extMap = new Hashtable<>(); + try { + boolean first = true; + while ((line = rd.readLine()) != null) { + if (first) { // skip first line which is the greeting + first = false; + continue; + } + if (line.length() < 5) + continue; // shouldn't happen + line = line.substring(4); // skip response code + int i = line.indexOf(' '); + String arg = ""; + if (i > 0) { + arg = line.substring(i + 1); + line = line.substring(0, i); + } + if (logger.isLoggable(Level.FINE)) + logger.fine("Found extension \"" + + line + "\", arg \"" + arg + "\""); + extMap.put(line.toUpperCase(Locale.ENGLISH), arg); + } + } catch (IOException ex) { } // can't happen + } + return resp == 250; + } + + /** + * Issue the MAIL FROM: command to start sending a message.

+ * + * Gets the sender's address in the following order: + *

    + *
  1. SMTPMessage.getEnvelopeFrom()
  2. + *
  3. mail.smtp.from property
  4. + *
  5. From: header in the message
  6. + *
  7. System username using the + * InternetAddress.getLocalAddress() method
  8. + *
+ * + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected void mailFrom() throws MessagingException { + String from = null; + if (message instanceof SMTPMessage) + from = ((SMTPMessage)message).getEnvelopeFrom(); + if (from == null || from.length() <= 0) + from = session.getProperty("mail." + name + ".from"); + if (from == null || from.length() <= 0) { + Address[] fa; + Address me; + if (message != null && (fa = message.getFrom()) != null && + fa.length > 0) + me = fa[0]; + else + me = InternetAddress.getLocalAddress(session); + + if (me != null) + from = ((InternetAddress)me).getAddress(); + else + throw new MessagingException( + "can't determine local email address"); + } + + String cmd = "MAIL FROM:" + normalizeAddress(from); + + if (allowutf8 && supportsExtension("SMTPUTF8")) + cmd += " SMTPUTF8"; + + // request delivery status notification? + if (supportsExtension("DSN")) { + String ret = null; + if (message instanceof SMTPMessage) + ret = ((SMTPMessage)message).getDSNRet(); + if (ret == null) + ret = session.getProperty("mail." + name + ".dsn.ret"); + // XXX - check for legal syntax? + if (ret != null) + cmd += " RET=" + ret; + } + + /* + * If an RFC 2554 submitter has been specified, and the server + * supports the AUTH extension, include the AUTH= element on + * the MAIL FROM command. + */ + if (supportsExtension("AUTH")) { + String submitter = null; + if (message instanceof SMTPMessage) + submitter = ((SMTPMessage)message).getSubmitter(); + if (submitter == null) + submitter = session.getProperty("mail." + name + ".submitter"); + // XXX - check for legal syntax? + if (submitter != null) { + try { + String s = xtext(submitter, + allowutf8 && supportsExtension("SMTPUTF8")); + cmd += " AUTH=" + s; + } catch (IllegalArgumentException ex) { + if (logger.isLoggable(Level.FINE)) + logger.log(Level.FINE, "ignoring invalid submitter: " + + submitter, ex); + } + } + } + + /* + * Have any extensions to the MAIL command been specified? + */ + String ext = null; + if (message instanceof SMTPMessage) + ext = ((SMTPMessage)message).getMailExtension(); + if (ext == null) + ext = session.getProperty("mail." + name + ".mailextension"); + if (ext != null && ext.length() > 0) + cmd += " " + ext; + + try { + issueSendCommand(cmd, 250); + } catch (SMTPSendFailedException ex) { + int retCode = ex.getReturnCode(); + switch (retCode) { + case 550: case 553: case 503: case 551: case 501: + // given address is invalid + try { + ex.setNextException(new SMTPSenderFailedException( + new InternetAddress(from), cmd, + retCode, ex.getMessage())); + } catch (AddressException aex) { + // oh well... + } + break; + default: + break; + } + throw ex; + } + } + + /** + * Sends each address to the SMTP host using the RCPT TO: + * command and copies the address either into + * the validSentAddr or invalidAddr arrays. + * Sets the sendFailed + * flag to true if any addresses failed. + * + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + /* + * success/failure/error possibilities from the RCPT command + * from rfc821, section 4.3 + * S: 250, 251 + * F: 550, 551, 552, 553, 450, 451, 452 + * E: 500, 501, 503, 421 + * + * and how we map the above error/failure conditions to valid/invalid + * address lists that are reported in the thrown exception: + * invalid addr: 550, 501, 503, 551, 553 + * valid addr: 552 (quota), 450, 451, 452 (quota), 421 (srvr abort) + */ + protected void rcptTo() throws MessagingException { + List valid = new ArrayList<>(); + List validUnsent = new ArrayList<>(); + List invalid = new ArrayList<>(); + int retCode = -1; + MessagingException mex = null; + boolean sendFailed = false; + MessagingException sfex = null; + validSentAddr = validUnsentAddr = invalidAddr = null; + boolean sendPartial = false; + if (message instanceof SMTPMessage) + sendPartial = ((SMTPMessage)message).getSendPartial(); + if (!sendPartial) + sendPartial = PropUtil.getBooleanProperty(session.getProperties(), + "mail." + name + ".sendpartial", false); + if (sendPartial) + logger.fine("sendPartial set"); + + boolean dsn = false; + String notify = null; + if (supportsExtension("DSN")) { + if (message instanceof SMTPMessage) + notify = ((SMTPMessage)message).getDSNNotify(); + if (notify == null) + notify = session.getProperty("mail." + name + ".dsn.notify"); + // XXX - check for legal syntax? + if (notify != null) + dsn = true; + } + + // try the addresses one at a time + for (int i = 0; i < addresses.length; i++) { + + sfex = null; + InternetAddress ia = (InternetAddress)addresses[i]; + String cmd = "RCPT TO:" + normalizeAddress(ia.getAddress()); + if (dsn) + cmd += " NOTIFY=" + notify; + // send the addresses to the SMTP server + sendCommand(cmd); + // check the server's response for address validity + retCode = readServerResponse(); + switch (retCode) { + case 250: case 251: + valid.add(ia); + if (!reportSuccess) + break; + + // user wants exception even when successful, including + // details of the return code + + // create and chain the exception + sfex = new SMTPAddressSucceededException(ia, cmd, retCode, + lastServerResponse); + if (mex == null) + mex = sfex; + else + mex.setNextException(sfex); + break; + + case 550: case 553: case 503: case 551: case 501: + // given address is invalid + if (!sendPartial) + sendFailed = true; + invalid.add(ia); + // create and chain the exception + sfex = new SMTPAddressFailedException(ia, cmd, retCode, + lastServerResponse); + if (mex == null) + mex = sfex; + else + mex.setNextException(sfex); + break; + + case 552: case 450: case 451: case 452: + // given address is valid + if (!sendPartial) + sendFailed = true; + validUnsent.add(ia); + // create and chain the exception + sfex = new SMTPAddressFailedException(ia, cmd, retCode, + lastServerResponse); + if (mex == null) + mex = sfex; + else + mex.setNextException(sfex); + break; + + default: + // handle remaining 4xy & 5xy codes + if (retCode >= 400 && retCode <= 499) { + // assume address is valid, although we don't really know + validUnsent.add(ia); + } else if (retCode >= 500 && retCode <= 599) { + // assume address is invalid, although we don't really know + invalid.add(ia); + } else { + // completely unexpected response, just give up + if (logger.isLoggable(Level.FINE)) + logger.fine("got response code " + retCode + + ", with response: " + lastServerResponse); + String _lsr = lastServerResponse; // else rset will nuke it + int _lrc = lastReturnCode; + if (serverSocket != null) // hasn't already been closed + issueCommand("RSET", -1); + lastServerResponse = _lsr; // restore, for get + lastReturnCode = _lrc; + throw new SMTPAddressFailedException(ia, cmd, retCode, + _lsr); + } + if (!sendPartial) + sendFailed = true; + // create and chain the exception + sfex = new SMTPAddressFailedException(ia, cmd, retCode, + lastServerResponse); + if (mex == null) + mex = sfex; + else + mex.setNextException(sfex); + break; + } + } + + // if we're willing to send to a partial list, and we found no + // valid addresses, that's complete failure + if (sendPartial && valid.size() == 0) + sendFailed = true; + + // copy the lists into appropriate arrays + if (sendFailed) { + // copy invalid addrs + invalidAddr = new Address[invalid.size()]; + invalid.toArray(invalidAddr); + + // copy all valid addresses to validUnsent, since something failed + validUnsentAddr = new Address[valid.size() + validUnsent.size()]; + int i = 0; + for (int j = 0; j < valid.size(); j++) + validUnsentAddr[i++] = (Address)valid.get(j); + for (int j = 0; j < validUnsent.size(); j++) + validUnsentAddr[i++] = (Address)validUnsent.get(j); + } else if (reportSuccess || (sendPartial && + (invalid.size() > 0 || validUnsent.size() > 0))) { + // we'll go on to send the message, but after sending we'll + // throw an exception with this exception nested + sendPartiallyFailed = true; + exception = mex; + + // copy invalid addrs + invalidAddr = new Address[invalid.size()]; + invalid.toArray(invalidAddr); + + // copy valid unsent addresses to validUnsent + validUnsentAddr = new Address[validUnsent.size()]; + validUnsent.toArray(validUnsentAddr); + + // copy valid addresses to validSent + validSentAddr = new Address[valid.size()]; + valid.toArray(validSentAddr); + } else { // all addresses pass + validSentAddr = addresses; + } + + + // print out the debug info + if (logger.isLoggable(Level.FINE)) { + if (validSentAddr != null && validSentAddr.length > 0) { + logger.fine("Verified Addresses"); + for (int l = 0; l < validSentAddr.length; l++) { + logger.fine(" " + validSentAddr[l]); + } + } + if (validUnsentAddr != null && validUnsentAddr.length > 0) { + logger.fine("Valid Unsent Addresses"); + for (int j = 0; j < validUnsentAddr.length; j++) { + logger.fine(" " + validUnsentAddr[j]); + } + } + if (invalidAddr != null && invalidAddr.length > 0) { + logger.fine("Invalid Addresses"); + for (int k = 0; k < invalidAddr.length; k++) { + logger.fine(" " + invalidAddr[k]); + } + } + } + + // throw the exception, fire TransportEvent.MESSAGE_NOT_DELIVERED event + if (sendFailed) { + logger.fine( + "Sending failed because of invalid destination addresses"); + notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, + validSentAddr, validUnsentAddr, + invalidAddr, this.message); + + // reset the connection so more sends are allowed + String lsr = lastServerResponse; // save, for get + int lrc = lastReturnCode; + try { + if (serverSocket != null) + issueCommand("RSET", -1); + } catch (MessagingException ex) { + // if can't reset, best to close the connection + try { + close(); + } catch (MessagingException ex2) { + // thrown by close()--ignore, will close() later anyway + logger.log(Level.FINE, "close failed", ex2); + } + } finally { + lastServerResponse = lsr; // restore + lastReturnCode = lrc; + } + + throw new SendFailedException("Invalid Addresses", mex, + validSentAddr, + validUnsentAddr, invalidAddr); + } + } + + /** + * Send the DATA command to the SMTP host and return + * an OutputStream to which the data is to be written. + * + * @return the stream to write to + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected OutputStream data() throws MessagingException { + assert Thread.holdsLock(this); + issueSendCommand("DATA", 354); + dataStream = new SMTPOutputStream(serverOutput); + return dataStream; + } + + /** + * Terminate the sent data. + * + * @exception IOException for I/O errors + * @exception MessagingException for other failures + * @since JavaMail 1.4.1 + */ + protected void finishData() throws IOException, MessagingException { + assert Thread.holdsLock(this); + dataStream.ensureAtBOL(); + issueSendCommand(".", 250); + } + + /** + * Return a stream that will use the SMTP BDAT command to send data. + * + * @return the stream to write to + * @exception MessagingException for failures + * @since JavaMail 1.6.0 + */ + protected OutputStream bdat() throws MessagingException { + assert Thread.holdsLock(this); + dataStream = new BDATOutputStream(serverOutput, chunkSize); + return dataStream; + } + + /** + * Terminate the sent data. + * + * @exception IOException for I/O errors + * @exception MessagingException for other failures + * @since JavaMail 1.6.0 + */ + protected void finishBdat() throws IOException, MessagingException { + assert Thread.holdsLock(this); + dataStream.ensureAtBOL(); + dataStream.close(); // doesn't close underlying socket + } + + /** + * Issue the STARTTLS command and switch the socket to + * TLS mode if it succeeds. + * + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected void startTLS() throws MessagingException { + issueCommand("STARTTLS", 220); + // it worked, now switch the socket into TLS mode + try { + serverSocket = SocketFetcher.startTLS(serverSocket, host, + session.getProperties(), "mail." + name); + initStreams(); + } catch (IOException ioex) { + closeConnection(); + throw new MessagingException("Could not convert socket to TLS", + ioex); + } + } + + /////// primitives /////// + + /** + * Connect to host on port and start the SMTP protocol. + */ + private void openServer(String host, int port) + throws MessagingException { + + if (logger.isLoggable(Level.FINE)) + logger.fine("trying to connect to host \"" + host + + "\", port " + port + ", isSSL " + isSSL); + + try { + Properties props = session.getProperties(); + + serverSocket = SocketFetcher.getSocket(host, port, + props, "mail." + name, isSSL); + + // socket factory may've chosen a different port, + // update it for the debug messages that follow + port = serverSocket.getPort(); + // save host name for startTLS + this.host = host; + + initStreams(); + + int r = -1; + if ((r = readServerResponse()) != 220) { + String failResponse = lastServerResponse; + try { + if (quitOnSessionReject) { + sendCommand("QUIT"); + if (quitWait) { + int resp = readServerResponse(); + if (resp != 221 && resp != -1 && + logger.isLoggable(Level.FINE)) + logger.fine("QUIT failed with " + resp); + } + } + } catch (Exception e) { + if (logger.isLoggable(Level.FINE)) + logger.log(Level.FINE, "QUIT failed", e); + } finally { + serverSocket.close(); + serverSocket = null; + serverOutput = null; + serverInput = null; + lineInputStream = null; + } + if (logger.isLoggable(Level.FINE)) + logger.fine("got bad greeting from host \"" + + host + "\", port: " + port + + ", response: " + failResponse); + throw new MessagingException( + "Got bad greeting from SMTP host: " + host + + ", port: " + port + + ", response: " + failResponse); + } else { + if (logger.isLoggable(Level.FINE)) + logger.fine("connected to host \"" + + host + "\", port: " + port); + } + } catch (UnknownHostException uhex) { + throw new MessagingException("Unknown SMTP host: " + host, uhex); + } catch (SocketConnectException scex) { + throw new MailConnectException(scex); + } catch (IOException ioe) { + throw new MessagingException("Could not connect to SMTP host: " + + host + ", port: " + port, ioe); + } + } + + /** + * Start the protocol to the server on serverSocket, + * assumed to be provided and connected by the caller. + */ + private void openServer() throws MessagingException { + int port = -1; + host = "UNKNOWN"; + try { + port = serverSocket.getPort(); + host = serverSocket.getInetAddress().getHostName(); + if (logger.isLoggable(Level.FINE)) + logger.fine("starting protocol to host \"" + + host + "\", port " + port); + + initStreams(); + + int r = -1; + if ((r = readServerResponse()) != 220) { + try { + if (quitOnSessionReject) { + sendCommand("QUIT"); + if (quitWait) { + int resp = readServerResponse(); + if (resp != 221 && resp != -1 && + logger.isLoggable(Level.FINE)) + logger.fine("QUIT failed with " + resp); + } + } + } catch (Exception e) { + if (logger.isLoggable(Level.FINE)) + logger.log(Level.FINE, "QUIT failed", e); + } finally { + serverSocket.close(); + serverSocket = null; + serverOutput = null; + serverInput = null; + lineInputStream = null; + } + if (logger.isLoggable(Level.FINE)) + logger.fine("got bad greeting from host \"" + + host + "\", port: " + port + + ", response: " + r); + throw new MessagingException( + "Got bad greeting from SMTP host: " + host + + ", port: " + port + + ", response: " + r); + } else { + if (logger.isLoggable(Level.FINE)) + logger.fine("protocol started to host \"" + + host + "\", port: " + port); + } + } catch (IOException ioe) { + throw new MessagingException( + "Could not start protocol to SMTP host: " + + host + ", port: " + port, ioe); + } + } + + + private void initStreams() throws IOException { + boolean quote = PropUtil.getBooleanProperty(session.getProperties(), + "mail.debug.quote", false); + + traceInput = + new TraceInputStream(serverSocket.getInputStream(), traceLogger); + traceInput.setQuote(quote); + + traceOutput = + new TraceOutputStream(serverSocket.getOutputStream(), traceLogger); + traceOutput.setQuote(quote); + + serverOutput = + new BufferedOutputStream(traceOutput); + serverInput = + new BufferedInputStream(traceInput); + lineInputStream = new LineInputStream(serverInput); + } + + /** + * Is protocol tracing enabled? + */ + private boolean isTracing() { + return traceLogger.isLoggable(Level.FINEST); + } + + /** + * Temporarily turn off protocol tracing, e.g., to prevent + * tracing the authentication sequence, including the password. + */ + private void suspendTracing() { + if (traceLogger.isLoggable(Level.FINEST)) { + traceInput.setTrace(false); + traceOutput.setTrace(false); + } + } + + /** + * Resume protocol tracing, if it was enabled to begin with. + */ + private void resumeTracing() { + if (traceLogger.isLoggable(Level.FINEST)) { + traceInput.setTrace(true); + traceOutput.setTrace(true); + } + } + + /** + * Send the command to the server. If the expected response code + * is not received, throw a MessagingException. + * + * @param cmd the command to send + * @param expect the expected response code (-1 means don't care) + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + public synchronized void issueCommand(String cmd, int expect) + throws MessagingException { + sendCommand(cmd); + + // if server responded with an unexpected return code, + // throw the exception, notifying the client of the response + int resp = readServerResponse(); + if (expect != -1 && resp != expect) + throw new MessagingException(lastServerResponse); + } + + /** + * Issue a command that's part of sending a message. + */ + private void issueSendCommand(String cmd, int expect) + throws MessagingException { + sendCommand(cmd); + + // if server responded with an unexpected return code, + // throw the exception, notifying the client of the response + int ret; + if ((ret = readServerResponse()) != expect) { + // assume message was not sent to anyone, + // combine valid sent & unsent addresses + int vsl = validSentAddr == null ? 0 : validSentAddr.length; + int vul = validUnsentAddr == null ? 0 : validUnsentAddr.length; + Address[] valid = new Address[vsl + vul]; + if (vsl > 0) + System.arraycopy(validSentAddr, 0, valid, 0, vsl); + if (vul > 0) + System.arraycopy(validUnsentAddr, 0, valid, vsl, vul); + validSentAddr = null; + validUnsentAddr = valid; + if (logger.isLoggable(Level.FINE)) + logger.fine("got response code " + ret + + ", with response: " + lastServerResponse); + String _lsr = lastServerResponse; // else rset will nuke it + int _lrc = lastReturnCode; + if (serverSocket != null) // hasn't already been closed + issueCommand("RSET", -1); + lastServerResponse = _lsr; // restore, for get + lastReturnCode = _lrc; + throw new SMTPSendFailedException(cmd, ret, lastServerResponse, + exception, validSentAddr, validUnsentAddr, invalidAddr); + } + } + + /** + * Send the command to the server and return the response code + * from the server. + * + * @param cmd the command + * @return the response code + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + public synchronized int simpleCommand(String cmd) + throws MessagingException { + sendCommand(cmd); + return readServerResponse(); + } + + /** + * Send the command to the server and return the response code + * from the server. + * + * @param cmd the command + * @return the response code + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected int simpleCommand(byte[] cmd) throws MessagingException { + assert Thread.holdsLock(this); + sendCommand(cmd); + return readServerResponse(); + } + + /** + * Sends command cmd to the server terminating + * it with CRLF. + * + * @param cmd the command + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected void sendCommand(String cmd) throws MessagingException { + sendCommand(toBytes(cmd)); + } + + private void sendCommand(byte[] cmdBytes) throws MessagingException { + assert Thread.holdsLock(this); + //if (logger.isLoggable(Level.FINE)) + //logger.fine("SENT: " + new String(cmdBytes, 0)); + + try { + serverOutput.write(cmdBytes); + serverOutput.write(CRLF); + serverOutput.flush(); + } catch (IOException ex) { + throw new MessagingException("Can't send command to SMTP host", ex); + } + } + + /** + * Reads server reponse returning the returnCode + * as the number. Returns -1 on failure. Sets + * lastServerResponse and lastReturnCode. + * + * @return server response code + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected int readServerResponse() throws MessagingException { + assert Thread.holdsLock(this); + String serverResponse = ""; + int returnCode = 0; + StringBuilder buf = new StringBuilder(100); + + // read the server response line(s) and add them to the buffer + // that stores the response + try { + String line = null; + + do { + line = lineInputStream.readLine(); + if (line == null) { + serverResponse = buf.toString(); + if (serverResponse.length() == 0) + serverResponse = "[EOF]"; + lastServerResponse = serverResponse; + lastReturnCode = -1; + logger.log(Level.FINE, "EOF: {0}", serverResponse); + return -1; + } + buf.append(line); + buf.append("\n"); + } while (isNotLastLine(line)); + + serverResponse = buf.toString(); + } catch (IOException ioex) { + logger.log(Level.FINE, "exception reading response", ioex); + //ioex.printStackTrace(out); + lastServerResponse = ""; + lastReturnCode = 0; + throw new MessagingException("Exception reading response", ioex); + //returnCode = -1; + } + + // print debug info + //if (logger.isLoggable(Level.FINE)) + //logger.fine("RCVD: " + serverResponse); + + // parse out the return code + if (serverResponse.length() >= 3) { + try { + returnCode = Integer.parseInt(serverResponse.substring(0, 3)); + } catch (NumberFormatException nfe) { + try { + close(); + } catch (MessagingException mex) { + // thrown by close()--ignore, will close() later anyway + logger.log(Level.FINE, "close failed", mex); + } + returnCode = -1; + } catch (StringIndexOutOfBoundsException ex) { + try { + close(); + } catch (MessagingException mex) { + // thrown by close()--ignore, will close() later anyway + logger.log(Level.FINE, "close failed", mex); + } + returnCode = -1; + } + } else { + returnCode = -1; + } + if (returnCode == -1) + logger.log(Level.FINE, "bad server response: {0}", serverResponse); + + lastServerResponse = serverResponse; + lastReturnCode = returnCode; + return returnCode; + } + + /** + * Check if we're in the connected state. Don't bother checking + * whether the server is still alive, that will be detected later. + * + * @exception IllegalStateException if not connected + * + * @since JavaMail 1.4.1 + */ + protected void checkConnected() { + if (!super.isConnected()) + throw new IllegalStateException("Not connected"); + } + + // tests if the line is an intermediate line according to SMTP + private boolean isNotLastLine(String line) { + return line != null && line.length() >= 4 && line.charAt(3) == '-'; + } + + // wraps an address in "<>"'s if necessary + private String normalizeAddress(String addr) { + if ((!addr.startsWith("<")) && (!addr.endsWith(">"))) + return "<" + addr + ">"; + else + return addr; + } + + /** + * Return true if the SMTP server supports the specified service + * extension. Extensions are reported as results of the EHLO + * command when connecting to the server. See + * RFC 1869 + * and other RFCs that define specific extensions. + * + * @param ext the service extension name + * @return true if the extension is supported + * + * @since JavaMail 1.3.2 + */ + public boolean supportsExtension(String ext) { + return extMap != null && + extMap.get(ext.toUpperCase(Locale.ENGLISH)) != null; + } + + /** + * Return the parameter the server provided for the specified + * service extension, or null if the extension isn't supported. + * + * @param ext the service extension name + * @return the extension parameter + * + * @since JavaMail 1.3.2 + */ + public String getExtensionParameter(String ext) { + return extMap == null ? null : + extMap.get(ext.toUpperCase(Locale.ENGLISH)); + } + + /** + * Does the server we're connected to support the specified + * authentication mechanism? Uses the extension information + * returned by the server from the EHLO command. + * + * @param auth the authentication mechanism + * @return true if the authentication mechanism is supported + * + * @since JavaMail 1.4.1 + */ + protected boolean supportsAuthentication(String auth) { + assert Thread.holdsLock(this); + if (extMap == null) + return false; + String a = extMap.get("AUTH"); + if (a == null) + return false; + StringTokenizer st = new StringTokenizer(a); + while (st.hasMoreTokens()) { + String tok = st.nextToken(); + if (tok.equalsIgnoreCase(auth)) + return true; + } + // hack for buggy servers that advertise capability incorrectly + if (auth.equalsIgnoreCase("LOGIN") && supportsExtension("AUTH=LOGIN")) { + logger.fine("use AUTH=LOGIN hack"); + return true; + } + return false; + } + + private static char[] hexchar = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + + /** + * Convert a string to RFC 1891 xtext format. + * + *
+     *     xtext = *( xchar / hexchar )
+     *
+     *     xchar = any ASCII CHAR between "!" (33) and "~" (126) inclusive,
+     *          except for "+" and "=".
+     *
+     * ; "hexchar"s are intended to encode octets that cannot appear
+     * ; as ASCII characters within an esmtp-value.
+     *
+     *     hexchar = ASCII "+" immediately followed by two upper case
+     *          hexadecimal digits
+     * 
+ * + * @param s the string to convert + * @return the xtext format string + * @since JavaMail 1.4.1 + */ + // XXX - keeping this around only for compatibility + protected static String xtext(String s) { + return xtext(s, false); + } + + /** + * Like xtext(s), but allow UTF-8 strings. + * + * @param s the string to convert + * @param utf8 convert string to UTF-8 first? + * @return the xtext format string + * @since JavaMail 1.6.0 + */ + protected static String xtext(String s, boolean utf8) { + StringBuilder sb = null; + byte[] bytes; + if (utf8) + bytes = s.getBytes(StandardCharsets.UTF_8); + else + bytes = ASCIIUtility.getBytes(s); + for (int i = 0; i < bytes.length; i++) { + char c = (char)(((int)bytes[i])&0xff); + if (!utf8 && c >= 128) // not ASCII + throw new IllegalArgumentException( + "Non-ASCII character in SMTP submitter: " + s); + if (c < '!' || c > '~' || c == '+' || c == '=') { + // not printable ASCII + if (sb == null) { + sb = new StringBuilder(s.length() + 4); + sb.append(s.substring(0, i)); + } + sb.append('+'); + sb.append(hexchar[(((int)c)& 0xf0) >> 4]); + sb.append(hexchar[((int)c)& 0x0f]); + } else { + if (sb != null) + sb.append(c); + } + } + return sb != null ? sb.toString() : s; + } + + private String traceUser(String user) { + return debugusername ? user : ""; + } + + private String tracePassword(String password) { + return debugpassword ? password : + (password == null ? "" : ""); + } + + /** + * Convert the String to either ASCII or UTF-8 bytes + * depending on allowutf8. + */ + private byte[] toBytes(String s) { + if (allowutf8) + return s.getBytes(StandardCharsets.UTF_8); + else + // don't use StandardCharsets.US_ASCII because it rejects non-ASCII + return ASCIIUtility.getBytes(s); + } + + /* + * Probe points for GlassFish monitoring. + */ + private void sendMessageStart(String subject) { } + private void sendMessageEnd() { } + + + /** + * An SMTPOutputStream that wraps a ChunkedOutputStream. + */ + private class BDATOutputStream extends SMTPOutputStream { + + /** + * Create a BDATOutputStream that wraps a ChunkedOutputStream + * of the given size and built on top of the specified + * underlying output stream. + * + * @param out the underlying output stream + * @param size the chunk size + */ + public BDATOutputStream(OutputStream out, int size) { + super(new ChunkedOutputStream(out, size)); + } + + /** + * Close this output stream. + * + * @exception IOException for I/O errors + */ + @Override + public void close() throws IOException { + out.close(); + } + } + + /** + * An OutputStream that buffers data in chunks and uses the + * RFC 3030 BDAT SMTP command to send each chunk. + */ + private class ChunkedOutputStream extends OutputStream { + private final OutputStream out; + private final byte[] buf; + private int count = 0; + + /** + * Create a ChunkedOutputStream built on top of the specified + * underlying output stream. + * + * @param out the underlying output stream + * @param size the chunk size + */ + public ChunkedOutputStream(OutputStream out, int size) { + this.out = out; + buf = new byte[size]; + } + + /** + * Writes the specified byte to this output stream. + * + * @param b the byte to write + * @exception IOException for I/O errors + */ + @Override + public void write(int b) throws IOException { + buf[count++] = (byte)b; + if (count >= buf.length) + flush(); + } + + /** + * Writes len bytes to this output stream starting at off. + * + * @param b bytes to write + * @param off offset in array + * @param len number of bytes to write + * @exception IOException for I/O errors + */ + @Override + public void write(byte b[], int off, int len) throws IOException { + while (len > 0) { + int size = Math.min(buf.length - count, len); + if (size == buf.length) { + // avoid the copy + bdat(b, off, size, false); + } else { + System.arraycopy(b, off, buf, count, size); + count += size; + } + off += size; + len -= size; + if (count >= buf.length) + flush(); + } + } + + /** + * Flush this output stream. + * + * @exception IOException for I/O errors + */ + @Override + public void flush() throws IOException { + bdat(buf, 0, count, false); + count = 0; + } + + /** + * Close this output stream. + * + * @exception IOException for I/O errors + */ + @Override + public void close() throws IOException { + bdat(buf, 0, count, true); + count = 0; + } + + /** + * Send the specified bytes using the BDAT command. + */ + private void bdat(byte[] b, int off, int len, boolean last) + throws IOException { + if (len > 0 || last) { + try { + if (last) + sendCommand("BDAT " + len + " LAST"); + else + sendCommand("BDAT " + len); + out.write(b, off, len); + out.flush(); + int ret = readServerResponse(); + if (ret != 250) + throw new IOException(lastServerResponse); + } catch (MessagingException mex) { + throw new IOException("BDAT write exception", mex); + } + } + } + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SaslAuthenticator.java b/app/src/main/java/com/sun/mail/smtp/SaslAuthenticator.java new file mode 100644 index 0000000000..2c2a645ebd --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SaslAuthenticator.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.MessagingException; + +/** + * Interface to make it easier to call SMTPSaslAuthenticator. + */ + +public interface SaslAuthenticator { + public boolean authenticate(String[] mechs, String realm, String authzid, + String u, String p) throws MessagingException; + +} diff --git a/app/src/main/java/com/sun/mail/smtp/package.html b/app/src/main/java/com/sun/mail/smtp/package.html new file mode 100644 index 0000000000..197755c64c --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/package.html @@ -0,0 +1,835 @@ + + + + + + +com.sun.mail.smtp package + + + +An SMTP protocol provider for the Jakarta Mail API +that provides access to an SMTP server. +Refer to RFC 821 +for more information. +

+When sending a message, detailed information on each address that +fails is available in an +{@link com.sun.mail.smtp.SMTPAddressFailedException SMTPAddressFailedException} +chained off the top level +{@link javax.mail.SendFailedException SendFailedException} +that is thrown. +In addition, if the mail.smtp.reportsuccess property +is set, an +{@link com.sun.mail.smtp.SMTPAddressSucceededException +SMTPAddressSucceededException} +will be included in the list for each address that is successful. +Note that this will cause a top level +{@link javax.mail.SendFailedException SendFailedException} +to be thrown even though the send was successful. +

+

+The SMTP provider also supports ESMTP +(RFC 1651). +It can optionally use SMTP Authentication +(RFC 2554) +using the LOGIN, PLAIN, DIGEST-MD5, and NTLM mechanisms +(RFC 4616 +and RFC 2831). +

+

+To use SMTP authentication you'll need to set the mail.smtp.auth +property (see below) or provide the SMTP Transport +with a username and password when connecting to the SMTP server. You +can do this using one of the following approaches: +

+
    +
  • +

    +Provide an Authenticator object when creating your mail Session +and provide the username and password information during the +Authenticator callback. +

    +

    +Note that the mail.smtp.user property can be set to provide a +default username for the callback, but the password will still need to be +supplied explicitly. +

    +

    +This approach allows you to use the static Transport send method +to send messages. +

    +
  • +
  • +

    +Call the Transport connect method explicitly with username and +password arguments. +

    +

    +This approach requires you to explicitly manage a Transport object +and use the Transport sendMessage method to send the message. +The transport.java demo program demonstrates how to manage a Transport +object. The following is roughly equivalent to the static +Transport send method, but supplies the needed username and +password: +

    +
    +Transport tr = session.getTransport("smtp");
    +tr.connect(smtphost, username, password);
    +msg.saveChanges();	// don't forget this
    +tr.sendMessage(msg, msg.getAllRecipients());
    +tr.close();
    +
    +
  • +
+

+When using DIGEST-MD5 authentication, +you'll also need to supply an appropriate realm; +your mail server administrator can supply this information. +You can set this using the mail.smtp.sasl.realm property, +or the setSASLRealm method on SMTPTransport. +

+

+The SMTP protocol provider can use SASL +(RFC 2222) +authentication mechanisms on systems that support the +javax.security.sasl APIs, such as J2SE 5.0. +In addition to the SASL mechanisms that are built into +the SASL implementation, users can also provide additional +SASL mechanisms of their own design to support custom authentication +schemes. See the + +Java SASL API Programming and Deployment Guide for details. +Note that the current implementation doesn't support SASL mechanisms +that provide their own integrity or confidentiality layer. +

+

+Support for OAuth 2.0 authentication via the + +XOAUTH2 authentication mechanism is provided either through the SASL +support described above or as a built-in authentication mechanism in the +SMTP provider. +The OAuth 2.0 Access Token should be passed as the password for this mechanism. +See +OAuth2 Support for details. +

+

+SMTP can also optionally request Delivery Status Notifications +(RFC 1891). +The delivery status will typically be reported using +a "multipart/report" +(RFC 1892) +message type with a "message/delivery-status" +(RFC 1894) +part. +You can use the classes in the com.sun.mail.dsn package to +handle these MIME types. +Note that you'll need to include dsn.jar in your CLASSPATH +as this support is not included in mail.jar. +

+

+See below for the properties to enable these features. +

+

+Note also that THERE IS NOT SUFFICIENT DOCUMENTATION HERE TO USE THESE +FEATURES!!! You will need to read the appropriate RFCs mentioned above +to understand what these features do and how to use them. Don't just +start setting properties and then complain to us when it doesn't work +like you expect it to work. READ THE RFCs FIRST!!! +

+

+The SMTP protocol provider supports the CHUNKING extension defined in +RFC 3030. +Set the mail.smtp.chunksize property to the desired chunk +size in bytes. +If the server supports the CHUNKING extension, the BDAT command will be +used to send the message in chunksize pieces. Note that no pipelining is +done so this will be slower than sending the message in one piece. +Note also that the BINARYMIME extension described in RFC 3030 is NOT supported. +

+Properties +

+The SMTP protocol provider supports the following properties, +which may be set in the Jakarta Mail Session object. +The properties are always set as strings; the Type column describes +how the string is interpreted. For example, use +

+
+	props.put("mail.smtp.port", "888");
+
+

+to set the mail.smtp.port property, which is of type int. +

+

+Note that if you're using the "smtps" protocol to access SMTP over SSL, +all the properties would be named "mail.smtps.*". +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SMTP properties
NameTypeDescription
mail.smtp.userStringDefault user name for SMTP.
mail.smtp.hostStringThe SMTP server to connect to.
mail.smtp.portintThe SMTP server port to connect to, if the connect() method doesn't +explicitly specify one. Defaults to 25.
mail.smtp.connectiontimeoutintSocket connection timeout value in milliseconds. +This timeout is implemented by java.net.Socket. +Default is infinite timeout.
mail.smtp.timeoutintSocket read timeout value in milliseconds. +This timeout is implemented by java.net.Socket. +Default is infinite timeout.
mail.smtp.writetimeoutintSocket write timeout value in milliseconds. +This timeout is implemented by using a +java.util.concurrent.ScheduledExecutorService per connection +that schedules a thread to close the socket if the timeout expires. +Thus, the overhead of using this timeout is one thread per connection. +Default is infinite timeout.
mail.smtp.fromString +Email address to use for SMTP MAIL command. This sets the envelope +return address. Defaults to msg.getFrom() or +InternetAddress.getLocalAddress(). NOTE: mail.smtp.user was previously +used for this. +
mail.smtp.localhostString +Local host name used in the SMTP HELO or EHLO command. +Defaults to InetAddress.getLocalHost().getHostName(). +Should not normally need to +be set if your JDK and your name service are configured properly. +
mail.smtp.localaddressString +Local address (host name) to bind to when creating the SMTP socket. +Defaults to the address picked by the Socket class. +Should not normally need to be set, but useful with multi-homed hosts +where it's important to pick a particular local address to bind to. +
mail.smtp.localportint +Local port number to bind to when creating the SMTP socket. +Defaults to the port number picked by the Socket class. +
mail.smtp.ehloboolean +If false, do not attempt to sign on with the EHLO command. Defaults to +true. Normally failure of the EHLO command will fallback to the HELO +command; this property exists only for servers that don't fail EHLO +properly or don't implement EHLO properly. +
mail.smtp.authbooleanIf true, attempt to authenticate the user using the AUTH command. +Defaults to false.
mail.smtp.auth.mechanismsString +If set, lists the authentication mechanisms to consider, and the order +in which to consider them. Only mechanisms supported by the server and +supported by the current implementation will be used. +The default is "LOGIN PLAIN DIGEST-MD5 NTLM", which includes all +the authentication mechanisms supported by the current implementation +except XOAUTH2. +
mail.smtp.auth.login.disablebooleanIf true, prevents use of the AUTH LOGIN command. +Default is false.
mail.smtp.auth.plain.disablebooleanIf true, prevents use of the AUTH PLAIN command. +Default is false.
mail.smtp.auth.digest-md5.disablebooleanIf true, prevents use of the AUTH DIGEST-MD5 command. +Default is false.
mail.smtp.auth.ntlm.disablebooleanIf true, prevents use of the AUTH NTLM command. +Default is false.
mail.smtp.auth.ntlm.domainString +The NTLM authentication domain. +
mail.smtp.auth.ntlm.flagsint +NTLM protocol-specific flags. +See +http://curl.haxx.se/rfc/ntlm.html#theNtlmFlags for details. +
mail.smtp.auth.xoauth2.disablebooleanIf true, prevents use of the AUTHENTICATE XOAUTH2 command. +Because the OAuth 2.0 protocol requires a special access token instead of +a password, this mechanism is disabled by default. Enable it by explicitly +setting this property to "false" or by setting the "mail.smtp.auth.mechanisms" +property to "XOAUTH2".
mail.smtp.submitterStringThe submitter to use in the AUTH tag in the MAIL FROM command. +Typically used by a mail relay to pass along information about the +original submitter of the message. +See also the {@link com.sun.mail.smtp.SMTPMessage#setSubmitter setSubmitter} +method of {@link com.sun.mail.smtp.SMTPMessage SMTPMessage}. +Mail clients typically do not use this. +
mail.smtp.dsn.notifyStringThe NOTIFY option to the RCPT command. Either NEVER, or some +combination of SUCCESS, FAILURE, and DELAY (separated by commas).
mail.smtp.dsn.retStringThe RET option to the MAIL command. Either FULL or HDRS.
mail.smtp.allow8bitmimeboolean +If set to true, and the server supports the 8BITMIME extension, text +parts of messages that use the "quoted-printable" or "base64" encodings +are converted to use "8bit" encoding if they follow the RFC2045 rules +for 8bit text. +
mail.smtp.sendpartialboolean +If set to true, and a message has some valid and some invalid +addresses, send the message anyway, reporting the partial failure with +a SendFailedException. If set to false (the default), the message is +not sent to any of the recipients if there is an invalid recipient +address. +
mail.smtp.sasl.enableboolean +If set to true, attempt to use the javax.security.sasl package to +choose an authentication mechanism for login. +Defaults to false. +
mail.smtp.sasl.mechanismsString +A space or comma separated list of SASL mechanism names to try +to use. +
mail.smtp.sasl.authorizationidString +The authorization ID to use in the SASL authentication. +If not set, the authentication ID (user name) is used. +
mail.smtp.sasl.realmStringThe realm to use with DIGEST-MD5 authentication.
mail.smtp.sasl.usecanonicalhostnameboolean +If set to true, the canonical host name returned by +{@link java.net.InetAddress#getCanonicalHostName InetAddress.getCanonicalHostName} +is passed to the SASL mechanism, instead of the host name used to connect. +Defaults to false. +
mail.smtp.quitwaitboolean +If set to false, the QUIT command is sent +and the connection is immediately closed. +If set to true (the default), causes the transport to wait +for the response to the QUIT command. +
mail.smtp.quitonsessionrejectboolean +If set to false (the default), on session initiation rejection the QUIT +command is not sent and the connection is immediately closed. +If set to true, causes the transport to send the QUIT command prior to +closing the connection. +
mail.smtp.reportsuccessboolean +If set to true, causes the transport to include an +{@link com.sun.mail.smtp.SMTPAddressSucceededException +SMTPAddressSucceededException} +for each address that is successful. +Note also that this will cause a +{@link javax.mail.SendFailedException SendFailedException} +to be thrown from the +{@link com.sun.mail.smtp.SMTPTransport#sendMessage sendMessage} +method of +{@link com.sun.mail.smtp.SMTPTransport SMTPTransport} +even if all addresses were correct and the message was sent +successfully. +
mail.smtp.socketFactorySocketFactory +If set to a class that implements the +javax.net.SocketFactory interface, this class +will be used to create SMTP sockets. Note that this is an +instance of a class, not a name, and must be set using the +put method, not the setProperty method. +
mail.smtp.socketFactory.classString +If set, specifies the name of a class that implements the +javax.net.SocketFactory interface. This class +will be used to create SMTP sockets. +
mail.smtp.socketFactory.fallbackboolean +If set to true, failure to create a socket using the specified +socket factory class will cause the socket to be created using +the java.net.Socket class. +Defaults to true. +
mail.smtp.socketFactory.portint +Specifies the port to connect to when using the specified socket +factory. +If not set, the default port will be used. +
mail.smtp.ssl.enableboolean +If set to true, use SSL to connect and use the SSL port by default. +Defaults to false for the "smtp" protocol and true for the "smtps" protocol. +
mail.smtp.ssl.checkserveridentityboolean +If set to true, check the server identity as specified by +RFC 2595. +These additional checks based on the content of the server's certificate +are intended to prevent man-in-the-middle attacks. +Defaults to false. +
mail.smtp.ssl.trustString +If set, and a socket factory hasn't been specified, enables use of a +{@link com.sun.mail.util.MailSSLSocketFactory MailSSLSocketFactory}. +If set to "*", all hosts are trusted. +If set to a whitespace separated list of hosts, those hosts are trusted. +Otherwise, trust depends on the certificate the server presents. +
mail.smtp.ssl.socketFactorySSLSocketFactory +If set to a class that extends the +javax.net.ssl.SSLSocketFactory class, this class +will be used to create SMTP SSL sockets. Note that this is an +instance of a class, not a name, and must be set using the +put method, not the setProperty method. +
mail.smtp.ssl.socketFactory.classString +If set, specifies the name of a class that extends the +javax.net.ssl.SSLSocketFactory class. This class +will be used to create SMTP SSL sockets. +
mail.smtp.ssl.socketFactory.portint +Specifies the port to connect to when using the specified socket +factory. +If not set, the default port will be used. +
mail.smtp.ssl.protocolsstring +Specifies the SSL protocols that will be enabled for SSL connections. +The property value is a whitespace separated list of tokens acceptable +to the javax.net.ssl.SSLSocket.setEnabledProtocols method. +
mail.smtp.ssl.ciphersuitesstring +Specifies the SSL cipher suites that will be enabled for SSL connections. +The property value is a whitespace separated list of tokens acceptable +to the javax.net.ssl.SSLSocket.setEnabledCipherSuites method. +
mail.smtp.starttls.enableboolean +If true, enables the use of the STARTTLS command (if +supported by the server) to switch the connection to a TLS-protected +connection before issuing any login commands. +If the server does not support STARTTLS, the connection continues without +the use of TLS; see the +mail.smtp.starttls.required +property to fail if STARTTLS isn't supported. +Note that an appropriate trust store must configured so that the client +will trust the server's certificate. +Defaults to false. +
mail.smtp.starttls.requiredboolean +If true, requires the use of the STARTTLS command. +If the server doesn't support the STARTTLS command, or the command +fails, the connect method will fail. +Defaults to false. +
mail.smtp.proxy.hoststring +Specifies the host name of an HTTP web proxy server that will be used for +connections to the mail server. +
mail.smtp.proxy.portstring +Specifies the port number for the HTTP web proxy server. +Defaults to port 80. +
mail.smtp.proxy.userstring +Specifies the user name to use to authenticate with the HTTP web proxy server. +By default, no authentication is done. +
mail.smtp.proxy.passwordstring +Specifies the password to use to authenticate with the HTTP web proxy server. +By default, no authentication is done. +
mail.smtp.socks.hoststring +Specifies the host name of a SOCKS5 proxy server that will be used for +connections to the mail server. +
mail.smtp.socks.portstring +Specifies the port number for the SOCKS5 proxy server. +This should only need to be used if the proxy server is not using +the standard port number of 1080. +
mail.smtp.mailextensionString +Extension string to append to the MAIL command. +The extension string can be used to specify standard SMTP +service extensions as well as vendor-specific extensions. +Typically the application should use the +{@link com.sun.mail.smtp.SMTPTransport SMTPTransport} +method {@link com.sun.mail.smtp.SMTPTransport#supportsExtension +supportsExtension} +to verify that the server supports the desired service extension. +See RFC 1869 +and other RFCs that define specific extensions. +
mail.smtp.usersetboolean +If set to true, use the RSET command instead of the NOOP command +in the {@link javax.mail.Transport#isConnected isConnected} method. +In some cases sendmail will respond slowly after many NOOP commands; +use of RSET avoids this sendmail issue. +Defaults to false. +
mail.smtp.noop.strictboolean +If set to true (the default), insist on a 250 response code from the NOOP +command to indicate success. The NOOP command is used by the +{@link javax.mail.Transport#isConnected isConnected} method to determine +if the connection is still alive. +Some older servers return the wrong response code on success, some +servers don't implement the NOOP command at all and so always return +a failure code. Set this property to false to handle servers +that are broken in this way. +Normally, when a server times out a connection, it will send a 421 +response code, which the client will see as the response to the next +command it issues. +Some servers send the wrong failure response code when timing out a +connection. +Do not set this property to false when dealing with servers that are +broken in this way. +
+

+In general, applications should not need to use the classes in this +package directly. Instead, they should use the APIs defined by +javax.mail package (and subpackages). Applications should +never construct instances of SMTPTransport directly. +Instead, they should use the +Session method getTransport to acquire an +appropriate Transport object. +

+

+In addition to printing debugging output as controlled by the +{@link javax.mail.Session Session} configuration, +the com.sun.mail.smtp provider logs the same information using +{@link java.util.logging.Logger} as described in the following table: +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
SMTP Loggers
Logger NameLogging LevelPurpose
com.sun.mail.smtpCONFIGConfiguration of the SMTPTransport
com.sun.mail.smtpFINEGeneral debugging output
com.sun.mail.smtp.protocolFINESTComplete protocol trace
+

+WARNING: The APIs unique to this package should be +considered EXPERIMENTAL. They may be changed in the +future in ways that are incompatible with applications using the +current APIs. +

+ + + diff --git a/app/src/main/java/com/sun/mail/util/ASCIIUtility.java b/app/src/main/java/com/sun/mail/util/ASCIIUtility.java new file mode 100644 index 0000000000..4ab7a5576d --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/ASCIIUtility.java @@ -0,0 +1,284 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.IOException; + +public class ASCIIUtility { + + // Private constructor so that this class is not instantiated + private ASCIIUtility() { } + + /** + * Convert the bytes within the specified range of the given byte + * array into a signed integer in the given radix . The range extends + * from start till, but not including end.

+ * + * Based on java.lang.Integer.parseInt() + * + * @param b the bytes + * @param start the first byte offset + * @param end the last byte offset + * @param radix the radix + * @return the integer value + * @exception NumberFormatException for conversion errors + */ + public static int parseInt(byte[] b, int start, int end, int radix) + throws NumberFormatException { + if (b == null) + throw new NumberFormatException("null"); + + int result = 0; + boolean negative = false; + int i = start; + int limit; + int multmin; + int digit; + + if (end > start) { + if (b[i] == '-') { + negative = true; + limit = Integer.MIN_VALUE; + i++; + } else { + limit = -Integer.MAX_VALUE; + } + multmin = limit / radix; + if (i < end) { + digit = Character.digit((char)b[i++], radix); + if (digit < 0) { + throw new NumberFormatException( + "illegal number: " + toString(b, start, end) + ); + } else { + result = -digit; + } + } + while (i < end) { + // Accumulating negatively avoids surprises near MAX_VALUE + digit = Character.digit((char)b[i++], radix); + if (digit < 0) { + throw new NumberFormatException("illegal number"); + } + if (result < multmin) { + throw new NumberFormatException("illegal number"); + } + result *= radix; + if (result < limit + digit) { + throw new NumberFormatException("illegal number"); + } + result -= digit; + } + } else { + throw new NumberFormatException("illegal number"); + } + if (negative) { + if (i > start + 1) { + return result; + } else { /* Only got "-" */ + throw new NumberFormatException("illegal number"); + } + } else { + return -result; + } + } + + /** + * Convert the bytes within the specified range of the given byte + * array into a signed integer . The range extends from + * start till, but not including end. + * + * @param b the bytes + * @param start the first byte offset + * @param end the last byte offset + * @return the integer value + * @exception NumberFormatException for conversion errors + */ + public static int parseInt(byte[] b, int start, int end) + throws NumberFormatException { + return parseInt(b, start, end, 10); + } + + /** + * Convert the bytes within the specified range of the given byte + * array into a signed long in the given radix . The range extends + * from start till, but not including end.

+ * + * Based on java.lang.Long.parseLong() + * + * @param b the bytes + * @param start the first byte offset + * @param end the last byte offset + * @param radix the radix + * @return the long value + * @exception NumberFormatException for conversion errors + */ + public static long parseLong(byte[] b, int start, int end, int radix) + throws NumberFormatException { + if (b == null) + throw new NumberFormatException("null"); + + long result = 0; + boolean negative = false; + int i = start; + long limit; + long multmin; + int digit; + + if (end > start) { + if (b[i] == '-') { + negative = true; + limit = Long.MIN_VALUE; + i++; + } else { + limit = -Long.MAX_VALUE; + } + multmin = limit / radix; + if (i < end) { + digit = Character.digit((char)b[i++], radix); + if (digit < 0) { + throw new NumberFormatException( + "illegal number: " + toString(b, start, end) + ); + } else { + result = -digit; + } + } + while (i < end) { + // Accumulating negatively avoids surprises near MAX_VALUE + digit = Character.digit((char)b[i++], radix); + if (digit < 0) { + throw new NumberFormatException("illegal number"); + } + if (result < multmin) { + throw new NumberFormatException("illegal number"); + } + result *= radix; + if (result < limit + digit) { + throw new NumberFormatException("illegal number"); + } + result -= digit; + } + } else { + throw new NumberFormatException("illegal number"); + } + if (negative) { + if (i > start + 1) { + return result; + } else { /* Only got "-" */ + throw new NumberFormatException("illegal number"); + } + } else { + return -result; + } + } + + /** + * Convert the bytes within the specified range of the given byte + * array into a signed long . The range extends from + * start till, but not including end.

+ * + * @param b the bytes + * @param start the first byte offset + * @param end the last byte offset + * @return the long value + * @exception NumberFormatException for conversion errors + */ + public static long parseLong(byte[] b, int start, int end) + throws NumberFormatException { + return parseLong(b, start, end, 10); + } + + /** + * Convert the bytes within the specified range of the given byte + * array into a String. The range extends from start + * till, but not including end. + * + * @param b the bytes + * @param start the first byte offset + * @param end the last byte offset + * @return the String + */ + public static String toString(byte[] b, int start, int end) { + int size = end - start; + char[] theChars = new char[size]; + + for (int i = 0, j = start; i < size; ) + theChars[i++] = (char)(b[j++]&0xff); + + return new String(theChars); + } + + /** + * Convert the bytes into a String. + * + * @param b the bytes + * @return the String + * @since JavaMail 1.4.4 + */ + public static String toString(byte[] b) { + return toString(b, 0, b.length); + } + + public static String toString(ByteArrayInputStream is) { + int size = is.available(); + char[] theChars = new char[size]; + byte[] bytes = new byte[size]; + + is.read(bytes, 0, size); + for (int i = 0; i < size;) + theChars[i] = (char)(bytes[i++]&0xff); + + return new String(theChars); + } + + + public static byte[] getBytes(String s) { + char [] chars= s.toCharArray(); + int size = chars.length; + byte[] bytes = new byte[size]; + + for (int i = 0; i < size;) + bytes[i] = (byte) chars[i++]; + return bytes; + } + + public static byte[] getBytes(InputStream is) throws IOException { + + int len; + int size = 1024; + byte [] buf; + + + if (is instanceof ByteArrayInputStream) { + size = is.available(); + buf = new byte[size]; + len = is.read(buf, 0, size); + } + else { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + buf = new byte[size]; + while ((len = is.read(buf, 0, size)) != -1) + bos.write(buf, 0, len); + buf = bos.toByteArray(); + } + return buf; + } +} diff --git a/app/src/main/java/com/sun/mail/util/BASE64DecoderStream.java b/app/src/main/java/com/sun/mail/util/BASE64DecoderStream.java new file mode 100644 index 0000000000..2559c748a7 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/BASE64DecoderStream.java @@ -0,0 +1,456 @@ +/* + * Copyright (c) 1997, 2020 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a BASE64 Decoder. It is implemented as + * a FilterInputStream, so one can just wrap this class around + * any input stream and read bytes from this filter. The decoding + * is done as the bytes are read out. + * + * @author John Mani + * @author Bill Shannon + */ + +public class BASE64DecoderStream extends FilterInputStream { + // buffer of decoded bytes for single byte reads + private byte[] buffer = new byte[3]; + private int bufsize = 0; // size of the cache + private int index = 0; // index into the cache + + // buffer for almost 8K of typical 76 chars + CRLF lines, + // used by getByte method. this buffer contains encoded bytes. + private byte[] input_buffer = new byte[78*105]; + private int input_pos = 0; + private int input_len = 0;; + + private boolean ignoreErrors = false; + + /** + * Create a BASE64 decoder that decodes the specified input stream. + * The System property mail.mime.base64.ignoreerrors + * controls whether errors in the encoded data cause an exception + * or are ignored. The default is false (errors cause exception). + * + * @param in the input stream + */ + public BASE64DecoderStream(InputStream in) { + super(in); + // default to false + ignoreErrors = PropUtil.getBooleanSystemProperty( + "mail.mime.base64.ignoreerrors", false); + } + + /** + * Create a BASE64 decoder that decodes the specified input stream. + * + * @param in the input stream + * @param ignoreErrors ignore errors in encoded data? + */ + public BASE64DecoderStream(InputStream in, boolean ignoreErrors) { + super(in); + this.ignoreErrors = ignoreErrors; + } + + /** + * Read the next decoded byte from this input stream. The byte + * is returned as an int in the range 0 + * to 255. If no byte is available because the end of + * the stream has been reached, the value -1 is returned. + * This method blocks until input data is available, the end of the + * stream is detected, or an exception is thrown. + * + * @return next byte of data, or -1 if the end of the + * stream is reached. + * @exception IOException if an I/O error occurs. + * @see java.io.FilterInputStream#in + */ + @Override + public int read() throws IOException { + if (index >= bufsize) { + bufsize = decode(buffer, 0, buffer.length); + if (bufsize <= 0) // buffer is empty + return -1; + index = 0; // reset index into buffer + } + return buffer[index++] & 0xff; // Zero off the MSB + } + + /** + * Reads up to len decoded bytes of data from this input stream + * into an array of bytes. This method blocks until some input is + * available. + *

+ * + * @param buf the buffer into which the data is read. + * @param off the start offset of the data. + * @param len the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of + * the stream has been reached. + * @exception IOException if an I/O error occurs. + */ + @Override + public int read(byte[] buf, int off, int len) throws IOException { + if (len == 0) + return 0; + // empty out single byte read buffer + int off0 = off; + while (index < bufsize && len > 0) { + buf[off++] = buffer[index++]; + len--; + } + if (index >= bufsize) + bufsize = index = 0; + + int bsize = (len / 3) * 3; // round down to multiple of 3 bytes + if (bsize > 0) { + int size = decode(buf, off, bsize); + off += size; + len -= size; + + if (size != bsize) { // hit EOF? + if (off == off0) // haven't returned any data + return -1; + else // returned some data before hitting EOF + return off - off0; + } + } + + // finish up with a partial read if necessary + for (; len > 0; len--) { + int c = read(); + if (c == -1) // EOF + break; + buf[off++] = (byte)c; + } + + if (off == off0) // haven't returned any data + return -1; + else // returned some data before hitting EOF + return off - off0; + } + + /** + * Skips over and discards n bytes of data from this stream. + */ + @Override + public long skip(long n) throws IOException { + long skipped = 0; + while (n-- > 0 && read() >= 0) + skipped++; + return skipped; + } + + /** + * Tests if this input stream supports marks. Currently this class + * does not support marks + */ + @Override + public boolean markSupported() { + return false; // Maybe later .. + } + + /** + * Returns the number of bytes that can be read from this input + * stream without blocking. However, this figure is only + * a close approximation in case the original encoded stream + * contains embedded CRLFs; since the CRLFs are discarded, not decoded + */ + @Override + public int available() throws IOException { + // This is only an estimate, since in.available() + // might include CRLFs too .. + return ((in.available() * 3)/4 + (bufsize-index)); + } + + /** + * This character array provides the character to value map + * based on RFC1521. + */ + private final static char pem_array[] = { + 'A','B','C','D','E','F','G','H', // 0 + 'I','J','K','L','M','N','O','P', // 1 + 'Q','R','S','T','U','V','W','X', // 2 + 'Y','Z','a','b','c','d','e','f', // 3 + 'g','h','i','j','k','l','m','n', // 4 + 'o','p','q','r','s','t','u','v', // 5 + 'w','x','y','z','0','1','2','3', // 6 + '4','5','6','7','8','9','+','/' // 7 + }; + + private final static byte pem_convert_array[] = new byte[256]; + + static { + for (int i = 0; i < 255; i++) + pem_convert_array[i] = -1; + for (int i = 0; i < pem_array.length; i++) + pem_convert_array[pem_array[i]] = (byte)i; + } + + /** + * The decoder algorithm. Most of the complexity here is dealing + * with error cases. Returns the number of bytes decoded, which + * may be zero. Decoding is done by filling an int with 4 6-bit + * values by shifting them in from the bottom and then extracting + * 3 8-bit bytes from the int by shifting them out from the bottom. + * + * @param outbuf the buffer into which to put the decoded bytes + * @param pos position in the buffer to start filling + * @param len the number of bytes to fill + * @return the number of bytes filled, always a multiple + * of three, and may be zero + * @exception IOException if the data is incorrectly formatted + */ + private int decode(byte[] outbuf, int pos, int len) throws IOException { + int pos0 = pos; + while (len >= 3) { + /* + * We need 4 valid base64 characters before we start decoding. + * We skip anything that's not a valid base64 character (usually + * just CRLF). + */ + int got = 0; + int val = 0; + while (got < 4) { + int i = getByte(); + if (i == -1 || i == -2) { + boolean atEOF; + if (i == -1) { + if (got == 0) + return pos - pos0; + if (!ignoreErrors) + throw new DecodingException( + "BASE64Decoder: Error in encoded stream: " + + "needed 4 valid base64 characters " + + "but only got " + got + " before EOF" + + recentChars()); + atEOF = true; // don't read any more + } else { // i == -2 + // found a padding character, we're at EOF + // XXX - should do something to make EOF "sticky" + if (got < 2 && !ignoreErrors) + throw new DecodingException( + "BASE64Decoder: Error in encoded stream: " + + "needed at least 2 valid base64 characters," + + " but only got " + got + + " before padding character (=)" + + recentChars()); + + // didn't get any characters before padding character? + if (got == 0) + return pos - pos0; + atEOF = false; // need to keep reading + } + + // pad partial result with zeroes + + // how many bytes will we produce on output? + // (got always < 4, so size always < 3) + int size = got - 1; + if (size == 0) + size = 1; + + // handle the one padding character we've seen + got++; + val <<= 6; + + while (got < 4) { + if (!atEOF) { + // consume the rest of the padding characters, + // filling with zeroes + i = getByte(); + if (i == -1) { + if (!ignoreErrors) + throw new DecodingException( + "BASE64Decoder: Error in encoded " + + "stream: hit EOF while looking for " + + "padding characters (=)" + + recentChars()); + } else if (i != -2) { + if (!ignoreErrors) + throw new DecodingException( + "BASE64Decoder: Error in encoded " + + "stream: found valid base64 " + + "character after a padding character " + + "(=)" + recentChars()); + } + } + val <<= 6; + got++; + } + + // now pull out however many valid bytes we got + val >>= 8; // always skip first one + if (size == 2) + outbuf[pos + 1] = (byte)(val & 0xff); + val >>= 8; + outbuf[pos] = (byte)(val & 0xff); + // len -= size; // not needed, return below + pos += size; + return pos - pos0; + } else { + // got a valid byte + val <<= 6; + got++; + val |= i; + } + } + + // read 4 valid characters, now extract 3 bytes + outbuf[pos + 2] = (byte)(val & 0xff); + val >>= 8; + outbuf[pos + 1] = (byte)(val & 0xff); + val >>= 8; + outbuf[pos] = (byte)(val & 0xff); + len -= 3; + pos += 3; + } + return pos - pos0; + } + + /** + * Read the next valid byte from the input stream. + * Buffer lots of data from underlying stream in input_buffer, + * for efficiency. + * + * @return the next byte, -1 on EOF, or -2 if next byte is '=' + * (padding at end of encoded data) + */ + private int getByte() throws IOException { + int c; + do { + if (input_pos >= input_len) { + try { + input_len = in.read(input_buffer); + } catch (EOFException ex) { + return -1; + } + if (input_len <= 0) + return -1; + input_pos = 0; + } + // get the next byte in the buffer + c = input_buffer[input_pos++] & 0xff; + // is it a padding byte? + if (c == '=') + return -2; + // no, convert it + c = pem_convert_array[c]; + // loop until we get a legitimate byte + } while (c == -1); + return c; + } + + /** + * Return the most recent characters, for use in an error message. + */ + private String recentChars() { + // reach into the input buffer and extract up to 10 + // recent characters, to help in debugging. + String errstr = ""; + int nc = input_pos > 10 ? 10 : input_pos; + if (nc > 0) { + errstr += ", the " + nc + + " most recent characters were: \""; + for (int k = input_pos - nc; k < input_pos; k++) { + char c = (char)(input_buffer[k] & 0xff); + switch (c) { + case '\r': errstr += "\\r"; break; + case '\n': errstr += "\\n"; break; + case '\t': errstr += "\\t"; break; + default: + if (c >= ' ' && c < 0177) + errstr += c; + else + errstr += ("\\" + (int)c); + } + } + errstr += "\""; + } + return errstr; + } + + /** + * Base64 decode a byte array. No line breaks are allowed. + * This method is suitable for short strings, such as those + * in the IMAP AUTHENTICATE protocol, but not to decode the + * entire content of a MIME part. + * + * NOTE: inbuf may only contain valid base64 characters. + * Whitespace is not ignored. + * + * @param inbuf the byte array + * @return the decoded byte array + */ + public static byte[] decode(byte[] inbuf) { + int size = (inbuf.length / 4) * 3; + if (size == 0) + return inbuf; + + if (inbuf[inbuf.length - 1] == '=') { + size--; + if (inbuf[inbuf.length - 2] == '=') + size--; + } + byte[] outbuf = new byte[size]; + + int inpos = 0, outpos = 0; + size = inbuf.length; + while (size > 0) { + int val; + int osize = 3; + val = pem_convert_array[inbuf[inpos++] & 0xff]; + val <<= 6; + val |= pem_convert_array[inbuf[inpos++] & 0xff]; + val <<= 6; + if (inbuf[inpos] != '=') // End of this BASE64 encoding + val |= pem_convert_array[inbuf[inpos++] & 0xff]; + else + osize--; + val <<= 6; + if (inbuf[inpos] != '=') // End of this BASE64 encoding + val |= pem_convert_array[inbuf[inpos++] & 0xff]; + else + osize--; + if (osize > 2) + outbuf[outpos + 2] = (byte)(val & 0xff); + val >>= 8; + if (osize > 1) + outbuf[outpos + 1] = (byte)(val & 0xff); + val >>= 8; + outbuf[outpos] = (byte)(val & 0xff); + outpos += osize; + size -= 4; + } + return outbuf; + } + + /*** begin TEST program *** + public static void main(String argv[]) throws Exception { + FileInputStream infile = new FileInputStream(argv[0]); + BASE64DecoderStream decoder = new BASE64DecoderStream(infile); + int c; + + while ((c = decoder.read()) != -1) + System.out.print((char)c); + System.out.flush(); + } + *** end TEST program ***/ +} diff --git a/app/src/main/java/com/sun/mail/util/BASE64EncoderStream.java b/app/src/main/java/com/sun/mail/util/BASE64EncoderStream.java new file mode 100644 index 0000000000..036b534821 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/BASE64EncoderStream.java @@ -0,0 +1,306 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a BASE64 encoder. It is implemented as + * a FilterOutputStream, so one can just wrap this class around + * any output stream and write bytes into this filter. The encoding + * is done as the bytes are written out. + * + * @author John Mani + * @author Bill Shannon + */ + +public class BASE64EncoderStream extends FilterOutputStream { + private byte[] buffer; // cache of bytes that are yet to be encoded + private int bufsize = 0; // size of the cache + private byte[] outbuf; // line size output buffer + private int count = 0; // number of bytes that have been output + private int bytesPerLine; // number of bytes per line + private int lineLimit; // number of input bytes to output bytesPerLine + private boolean noCRLF = false; + + private static byte[] newline = new byte[] { '\r', '\n' }; + + /** + * Create a BASE64 encoder that encodes the specified output stream. + * + * @param out the output stream + * @param bytesPerLine number of bytes per line. The encoder inserts + * a CRLF sequence after the specified number of bytes, + * unless bytesPerLine is Integer.MAX_VALUE, in which + * case no CRLF is inserted. bytesPerLine is rounded + * down to a multiple of 4. + */ + public BASE64EncoderStream(OutputStream out, int bytesPerLine) { + super(out); + buffer = new byte[3]; + if (bytesPerLine == Integer.MAX_VALUE || bytesPerLine < 4) { + noCRLF = true; + bytesPerLine = 76; + } + bytesPerLine = (bytesPerLine / 4) * 4; // Rounded down to 4n + this.bytesPerLine = bytesPerLine; // save it + lineLimit = bytesPerLine / 4 * 3; + + if (noCRLF) { + outbuf = new byte[bytesPerLine]; + } else { + outbuf = new byte[bytesPerLine + 2]; + outbuf[bytesPerLine] = (byte)'\r'; + outbuf[bytesPerLine + 1] = (byte)'\n'; + } + } + + /** + * Create a BASE64 encoder that encodes the specified input stream. + * Inserts the CRLF sequence after outputting 76 bytes. + * + * @param out the output stream + */ + public BASE64EncoderStream(OutputStream out) { + this(out, 76); + } + + /** + * Encodes len bytes from the specified + * byte array starting at offset off to + * this output stream. + * + * @param b the data. + * @param off the start offset in the data. + * @param len the number of bytes to write. + * @exception IOException if an I/O error occurs. + */ + @Override + public synchronized void write(byte[] b, int off, int len) + throws IOException { + int end = off + len; + + // finish off incomplete coding unit + while (bufsize != 0 && off < end) + write(b[off++]); + + // finish off line + int blen = ((bytesPerLine - count) / 4) * 3; + if (off + blen <= end) { + // number of bytes that will be produced in outbuf + int outlen = encodedSize(blen); + if (!noCRLF) { + outbuf[outlen++] = (byte)'\r'; + outbuf[outlen++] = (byte)'\n'; + } + out.write(encode(b, off, blen, outbuf), 0, outlen); + off += blen; + count = 0; + } + + // do bulk encoding a line at a time. + for (; off + lineLimit <= end; off += lineLimit) + out.write(encode(b, off, lineLimit, outbuf)); + + // handle remaining partial line + if (off + 3 <= end) { + blen = end - off; + blen = (blen / 3) * 3; // round down + // number of bytes that will be produced in outbuf + int outlen = encodedSize(blen); + out.write(encode(b, off, blen, outbuf), 0, outlen); + off += blen; + count += outlen; + } + + // start next coding unit + for (; off < end; off++) + write(b[off]); + } + + /** + * Encodes b.length bytes to this output stream. + * + * @param b the data to be written. + * @exception IOException if an I/O error occurs. + */ + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + /** + * Encodes the specified byte to this output stream. + * + * @param c the byte. + * @exception IOException if an I/O error occurs. + */ + @Override + public synchronized void write(int c) throws IOException { + buffer[bufsize++] = (byte)c; + if (bufsize == 3) { // Encoding unit = 3 bytes + encode(); + bufsize = 0; + } + } + + /** + * Flushes this output stream and forces any buffered output bytes + * to be encoded out to the stream. + * + * @exception IOException if an I/O error occurs. + */ + @Override + public synchronized void flush() throws IOException { + if (bufsize > 0) { // If there's unencoded characters in the buffer .. + encode(); // .. encode them + bufsize = 0; + } + out.flush(); + } + + /** + * Forces any buffered output bytes to be encoded out to the stream + * and closes this output stream + */ + @Override + public synchronized void close() throws IOException { + flush(); + if (count > 0 && !noCRLF) { + out.write(newline); + out.flush(); + } + out.close(); + } + + /** This array maps the characters to their 6 bit values */ + private final static char pem_array[] = { + 'A','B','C','D','E','F','G','H', // 0 + 'I','J','K','L','M','N','O','P', // 1 + 'Q','R','S','T','U','V','W','X', // 2 + 'Y','Z','a','b','c','d','e','f', // 3 + 'g','h','i','j','k','l','m','n', // 4 + 'o','p','q','r','s','t','u','v', // 5 + 'w','x','y','z','0','1','2','3', // 6 + '4','5','6','7','8','9','+','/' // 7 + }; + + /** + * Encode the data stored in buffer. + * Uses outbuf to store the encoded + * data before writing. + * + * @exception IOException if an I/O error occurs. + */ + private void encode() throws IOException { + int osize = encodedSize(bufsize); + out.write(encode(buffer, 0, bufsize, outbuf), 0, osize); + // increment count + count += osize; + // If writing out this encoded unit caused overflow, + // start a new line. + if (count >= bytesPerLine) { + if (!noCRLF) + out.write(newline); + count = 0; + } + } + + /** + * Base64 encode a byte array. No line breaks are inserted. + * This method is suitable for short strings, such as those + * in the IMAP AUTHENTICATE protocol, but not to encode the + * entire content of a MIME part. + * + * @param inbuf the byte array + * @return the encoded byte array + */ + public static byte[] encode(byte[] inbuf) { + if (inbuf.length == 0) + return inbuf; + return encode(inbuf, 0, inbuf.length, null); + } + + /** + * Internal use only version of encode. Allow specifying which + * part of the input buffer to encode. If outbuf is non-null, + * it's used as is. Otherwise, a new output buffer is allocated. + */ + private static byte[] encode(byte[] inbuf, int off, int size, + byte[] outbuf) { + if (outbuf == null) + outbuf = new byte[encodedSize(size)]; + int inpos, outpos; + int val; + for (inpos = off, outpos = 0; size >= 3; size -= 3, outpos += 4) { + val = inbuf[inpos++] & 0xff; + val <<= 8; + val |= inbuf[inpos++] & 0xff; + val <<= 8; + val |= inbuf[inpos++] & 0xff; + outbuf[outpos+3] = (byte)pem_array[val & 0x3f]; + val >>= 6; + outbuf[outpos+2] = (byte)pem_array[val & 0x3f]; + val >>= 6; + outbuf[outpos+1] = (byte)pem_array[val & 0x3f]; + val >>= 6; + outbuf[outpos+0] = (byte)pem_array[val & 0x3f]; + } + // done with groups of three, finish up any odd bytes left + if (size == 1) { + val = inbuf[inpos++] & 0xff; + val <<= 4; + outbuf[outpos+3] = (byte)'='; // pad character; + outbuf[outpos+2] = (byte)'='; // pad character; + outbuf[outpos+1] = (byte)pem_array[val & 0x3f]; + val >>= 6; + outbuf[outpos+0] = (byte)pem_array[val & 0x3f]; + } else if (size == 2) { + val = inbuf[inpos++] & 0xff; + val <<= 8; + val |= inbuf[inpos++] & 0xff; + val <<= 2; + outbuf[outpos+3] = (byte)'='; // pad character; + outbuf[outpos+2] = (byte)pem_array[val & 0x3f]; + val >>= 6; + outbuf[outpos+1] = (byte)pem_array[val & 0x3f]; + val >>= 6; + outbuf[outpos+0] = (byte)pem_array[val & 0x3f]; + } + return outbuf; + } + + /** + * Return the corresponding encoded size for the given number + * of bytes, not including any CRLF. + */ + private static int encodedSize(int size) { + return ((size + 2) / 3) * 4; + } + + /*** begin TEST program + public static void main(String argv[]) throws Exception { + FileInputStream infile = new FileInputStream(argv[0]); + BASE64EncoderStream encoder = new BASE64EncoderStream(System.out); + int c; + + while ((c = infile.read()) != -1) + encoder.write(c); + encoder.close(); + } + *** end TEST program **/ +} diff --git a/app/src/main/java/com/sun/mail/util/BEncoderStream.java b/app/src/main/java/com/sun/mail/util/BEncoderStream.java new file mode 100644 index 0000000000..5a43dd688c --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/BEncoderStream.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a 'B' Encoder as defined by RFC2047 for + * encoding MIME headers. It subclasses the BASE64EncoderStream + * class. + * + * @author John Mani + */ + +public class BEncoderStream extends BASE64EncoderStream { + + /** + * Create a 'B' encoder that encodes the specified input stream. + * @param out the output stream + */ + public BEncoderStream(OutputStream out) { + super(out, Integer.MAX_VALUE); // MAX_VALUE is 2^31, should + // suffice (!) to indicate that + // CRLFs should not be inserted + } + + /** + * Returns the length of the encoded version of this byte array. + * + * @param b the byte array + * @return the length + */ + public static int encodedLength(byte[] b) { + return ((b.length + 2)/3) * 4; + } +} diff --git a/app/src/main/java/com/sun/mail/util/CRLFOutputStream.java b/app/src/main/java/com/sun/mail/util/CRLFOutputStream.java new file mode 100644 index 0000000000..7cca3d7cc8 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/CRLFOutputStream.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + + +/** + * Convert lines into the canonical format, that is, terminate lines with the + * CRLF sequence. + * + * @author John Mani + */ +public class CRLFOutputStream extends FilterOutputStream { + protected int lastb = -1; + protected boolean atBOL = true; // at beginning of line? + private static final byte[] newline = { (byte)'\r', (byte)'\n' }; + + public CRLFOutputStream(OutputStream os) { + super(os); + } + + @Override + public void write(int b) throws IOException { + if (b == '\r') { + writeln(); + } else if (b == '\n') { + if (lastb != '\r') + writeln(); + } else { + out.write(b); + atBOL = false; + } + lastb = b; + } + + @Override + public void write(byte b[]) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte b[], int off, int len) throws IOException { + int start = off; + + len += off; + for (int i = start; i < len ; i++) { + if (b[i] == '\r') { + out.write(b, start, i - start); + writeln(); + start = i + 1; + } else if (b[i] == '\n') { + if (lastb != '\r') { + out.write(b, start, i - start); + writeln(); + } + start = i + 1; + } + lastb = b[i]; + } + if ((len - start) > 0) { + out.write(b, start, len - start); + atBOL = false; + } + } + + /* + * Just write out a new line, something similar to out.println() + */ + public void writeln() throws IOException { + out.write(newline); + atBOL = true; + } +} diff --git a/app/src/main/java/com/sun/mail/util/DecodingException.java b/app/src/main/java/com/sun/mail/util/DecodingException.java new file mode 100644 index 0000000000..fddd84f8fc --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/DecodingException.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.IOException; + +/** + * A special IOException that indicates a failure to decode data due + * to an error in the formatting of the data. This allows applications + * to distinguish decoding errors from other I/O errors. + * + * @author Bill Shannon + */ + +public class DecodingException extends IOException { + + private static final long serialVersionUID = -6913647794421459390L; + + /** + * Constructor. + * + * @param s the exception message + */ + public DecodingException(String s) { + super(s); + } +} diff --git a/app/src/main/java/com/sun/mail/util/DefaultProvider.java b/app/src/main/java/com/sun/mail/util/DefaultProvider.java new file mode 100644 index 0000000000..c47e128782 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/DefaultProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.lang.annotation.*; + +/** + * Annotation to mark the default providers that are part of Jakarta Mail. + * DO NOT use this on any provider made available independently. + * + * @author Bill Shannon + * @since Jakarta Mail 1.6.4 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DefaultProvider { +} diff --git a/app/src/main/java/com/sun/mail/util/FolderClosedIOException.java b/app/src/main/java/com/sun/mail/util/FolderClosedIOException.java new file mode 100644 index 0000000000..5a4ef10ae4 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/FolderClosedIOException.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.IOException; +import javax.mail.Folder; + +/** + * A variant of FolderClosedException that can be thrown from methods + * that only throw IOException. The getContent method will catch this + * exception and translate it back to FolderClosedException. + * + * @author Bill Shannon + */ + +public class FolderClosedIOException extends IOException { + transient private Folder folder; + + private static final long serialVersionUID = 4281122580365555735L; + + /** + * Constructor + * @param folder the Folder + */ + public FolderClosedIOException(Folder folder) { + this(folder, null); + } + + /** + * Constructor + * @param folder the Folder + * @param message the detailed error message + */ + public FolderClosedIOException(Folder folder, String message) { + super(message); + this.folder = folder; + } + + /** + * Returns the dead Folder object + * + * @return the dead Folder + */ + public Folder getFolder() { + return folder; + } +} diff --git a/app/src/main/java/com/sun/mail/util/LineInputStream.java b/app/src/main/java/com/sun/mail/util/LineInputStream.java new file mode 100644 index 0000000000..cb068f3ffb --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/LineInputStream.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.CharacterCodingException; + +/** + * LineInputStream supports reading CRLF terminated lines that + * contain only US-ASCII characters from an input stream. Provides + * functionality that is similar to the deprecated + * DataInputStream.readLine(). Expected use is to read + * lines as String objects from an IMAP/SMTP/etc. stream.

+ * + * This class also supports UTF-8 data by calling the appropriate + * constructor. Or, if the System property mail.mime.allowutf8 + * is set to true, an attempt will be made to interpret the data as UTF-8, + * falling back to treating it as an 8-bit charset if that fails.

+ * + * LineInputStream is implemented as a FilterInputStream, so one can just + * wrap it around any input stream and read bytes from this filter. + * + * @author John Mani + * @author Bill Shannon + */ + +public class LineInputStream extends FilterInputStream { + + private boolean allowutf8; + private byte[] lineBuffer = null; // reusable byte buffer + private CharsetDecoder decoder; + + private static boolean defaultutf8 = + PropUtil.getBooleanSystemProperty("mail.mime.allowutf8", false); + private static int MAX_INCR = 1024*1024; // 1MB + + public LineInputStream(InputStream in) { + this(in, false); + } + + /** + * @param in the InputStream + * @param allowutf8 allow UTF-8 characters? + * @since JavaMail 1.6 + */ + public LineInputStream(InputStream in, boolean allowutf8) { + super(in); + this.allowutf8 = allowutf8; + if (!allowutf8 && defaultutf8) { + decoder = StandardCharsets.UTF_8.newDecoder(); + decoder.onMalformedInput(CodingErrorAction.REPORT); + decoder.onUnmappableCharacter(CodingErrorAction.REPORT); + } + } + + /** + * Read a line containing only ASCII characters from the input + * stream. A line is terminated by a CR or NL or CR-NL sequence. + * A common error is a CR-CR-NL sequence, which will also terminate + * a line. + * The line terminator is not returned as part of the returned + * String. Returns null if no data is available.

+ * + * This class is similar to the deprecated + * DataInputStream.readLine() + * + * @return the line + * @exception IOException for I/O errors + */ + @SuppressWarnings("deprecation") // for old String constructor + public String readLine() throws IOException { + //InputStream in = this.in; + byte[] buf = lineBuffer; + + if (buf == null) + buf = lineBuffer = new byte[128]; + + int c1; + int room = buf.length; + int offset = 0; + + while ((c1 = in.read()) != -1) { + if (c1 == '\n') // Got NL, outa here. + break; + else if (c1 == '\r') { + // Got CR, is the next char NL ? + boolean twoCRs = false; + if (in.markSupported()) + in.mark(2); + int c2 = in.read(); + if (c2 == '\r') { // discard extraneous CR + twoCRs = true; + c2 = in.read(); + } + if (c2 != '\n') { + /* + * If the stream supports it (which we hope will always + * be the case), reset to after the first CR. Otherwise, + * we wrap a PushbackInputStream around the stream so we + * can unread the characters we don't need. The only + * problem with that is that the caller might stop + * reading from this LineInputStream, throw it away, + * and then start reading from the underlying stream. + * If that happens, the pushed back characters will be + * lost forever. + */ + if (in.markSupported()) + in.reset(); + else { + if (!(in instanceof PushbackInputStream)) + in /*= this.in*/ = new PushbackInputStream(in, 2); + if (c2 != -1) + ((PushbackInputStream)in).unread(c2); + if (twoCRs) + ((PushbackInputStream)in).unread('\r'); + } + } + break; // outa here. + } + + // Not CR, NL or CR-NL ... + // .. Insert the byte into our byte buffer + if (--room < 0) { // No room, need to grow. + if (buf.length < MAX_INCR) + buf = new byte[buf.length * 2]; + else + buf = new byte[buf.length + MAX_INCR]; + room = buf.length - offset - 1; + System.arraycopy(lineBuffer, 0, buf, 0, offset); + lineBuffer = buf; + } + buf[offset++] = (byte)c1; + } + + if ((c1 == -1) && (offset == 0)) + return null; + + if (allowutf8) + return new String(buf, 0, offset, StandardCharsets.UTF_8); + else { + if (defaultutf8) { + // try to decode it as UTF-8 + try { + return decoder.decode(ByteBuffer.wrap(buf, 0, offset)). + toString(); + } catch (CharacterCodingException cex) { + // looks like it's not valid UTF-8 data, + // fall through and treat it as an 8-bit charset + } + } + return new String(buf, 0, 0, offset); + } + } +} diff --git a/app/src/main/java/com/sun/mail/util/LineOutputStream.java b/app/src/main/java/com/sun/mail/util/LineOutputStream.java new file mode 100644 index 0000000000..d86f5ec22a --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/LineOutputStream.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; +import java.nio.charset.StandardCharsets; + +/** + * This class is to support writing out Strings as a sequence of bytes + * terminated by a CRLF sequence. The String must contain only US-ASCII + * characters.

+ * + * The expected use is to write out RFC822 style headers to an output + * stream.

+ * + * @author John Mani + * @author Bill Shannon + */ + +public class LineOutputStream extends FilterOutputStream { + private boolean allowutf8; + + private static byte[] newline; + + static { + newline = new byte[2]; + newline[0] = (byte)'\r'; + newline[1] = (byte)'\n'; + } + + public LineOutputStream(OutputStream out) { + this(out, false); + } + + /** + * @param out the OutputStream + * @param allowutf8 allow UTF-8 characters? + * @since JavaMail 1.6 + */ + public LineOutputStream(OutputStream out, boolean allowutf8) { + super(out); + this.allowutf8 = allowutf8; + } + + public void writeln(String s) throws IOException { + byte[] bytes; + if (allowutf8) + bytes = s.getBytes(StandardCharsets.UTF_8); + else + bytes = ASCIIUtility.getBytes(s); + out.write(bytes); + out.write(newline); + } + + public void writeln() throws IOException { + out.write(newline); + } +} diff --git a/app/src/main/java/com/sun/mail/util/LogOutputStream.java b/app/src/main/java/com/sun/mail/util/LogOutputStream.java new file mode 100644 index 0000000000..c1c31531f9 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/LogOutputStream.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2008, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.logging.Level; + +/** + * Capture output lines and send them to the mail logger. + */ +public class LogOutputStream extends OutputStream { + protected MailLogger logger; + protected Level level; + + private int lastb = -1; + private byte[] buf = new byte[80]; + private int pos = 0; + + /** + * Log to the specified logger. + * + * @param logger the MailLogger + */ + public LogOutputStream(MailLogger logger) { + this.logger = logger; + this.level = Level.FINEST; + } + + @Override + public void write(int b) throws IOException { + if (!logger.isLoggable(level)) + return; + + if (b == '\r') { + logBuf(); + } else if (b == '\n') { + if (lastb != '\r') + logBuf(); + } else { + expandCapacity(1); + buf[pos++] = (byte)b; + } + lastb = b; + } + + @Override + public void write(byte b[]) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte b[], int off, int len) throws IOException { + int start = off; + + if (!logger.isLoggable(level)) + return; + len += off; + for (int i = start; i < len ; i++) { + if (b[i] == '\r') { + expandCapacity(i - start); + System.arraycopy(b, start, buf, pos, i - start); + pos += i - start; + logBuf(); + start = i + 1; + } else if (b[i] == '\n') { + if (lastb != '\r') { + expandCapacity(i - start); + System.arraycopy(b, start, buf, pos, i - start); + pos += i - start; + logBuf(); + } + start = i + 1; + } + lastb = b[i]; + } + if ((len - start) > 0) { + expandCapacity(len - start); + System.arraycopy(b, start, buf, pos, len - start); + pos += len - start; + } + } + + /** + * Log the specified message. + * Can be overridden by subclass to do different logging. + * + * @param msg the message to log + */ + protected void log(String msg) { + logger.log(level, msg); + } + + /** + * Convert the buffer to a string and log it. + */ + private void logBuf() { + String msg = new String(buf, 0, pos); + pos = 0; + log(msg); + } + + /** + * Ensure that the buffer can hold at least len bytes + * beyond the current position. + */ + private void expandCapacity(int len) { + while (pos + len > buf.length) { + byte[] nb = new byte[buf.length * 2]; + System.arraycopy(buf, 0, nb, 0, pos); + buf = nb; + } + } +} diff --git a/app/src/main/java/com/sun/mail/util/MailConnectException.java b/app/src/main/java/com/sun/mail/util/MailConnectException.java new file mode 100644 index 0000000000..768e90f4f7 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/MailConnectException.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import javax.mail.MessagingException; + +/** + * A MessagingException that indicates a socket connection attempt failed. + * Unlike java.net.ConnectException, it includes details of what we + * were trying to connect to. The underlying exception is available + * as the "cause" of this exception. + * + * @see java.net.ConnectException + * @author Bill Shannon + * @since JavaMail 1.5.0 + */ + +public class MailConnectException extends MessagingException { + private String host; + private int port; + private int cto; + + private static final long serialVersionUID = -3818807731125317729L; + + /** + * Constructs a MailConnectException. + * + * @param cex the SocketConnectException with the details + */ + public MailConnectException(SocketConnectException cex) { + super( + "Couldn't connect to host, port: " + + cex.getHost() + ", " + cex.getPort() + + "; timeout " + cex.getConnectionTimeout() + + (cex.getMessage() != null ? ("; " + cex.getMessage()) : "")); + // extract the details and save them here + this.host = cex.getHost(); + this.port = cex.getPort(); + this.cto = cex.getConnectionTimeout(); + setNextException(cex.getException()); + } + + /** + * The host we were trying to connect to. + * + * @return the host + */ + public String getHost() { + return host; + } + + /** + * The port we were trying to connect to. + * + * @return the port + */ + public int getPort() { + return port; + } + + /** + * The timeout used for the connection attempt. + * + * @return the connection timeout + */ + public int getConnectionTimeout() { + return cto; + } +} diff --git a/app/src/main/java/com/sun/mail/util/MailLogger.java b/app/src/main/java/com/sun/mail/util/MailLogger.java new file mode 100644 index 0000000000..686f5cab6f --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/MailLogger.java @@ -0,0 +1,419 @@ +/* + * Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.PrintStream; +import java.text.MessageFormat; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.mail.Session; + +/** + * A simplified logger used by Jakarta Mail to handle logging to a + * PrintStream and logging through a java.util.logging.Logger. + * If debug is set, messages are written to the PrintStream and + * prefixed by the specified prefix (which is not included in + * Logger messages). + * Messages are logged by the Logger based on the configuration + * of the logging system. + */ + +/* + * It would be so much simpler to just subclass Logger and override + * the log(LogRecord) method, as the javadocs suggest, but that doesn't + * work because Logger makes the decision about whether to log the message + * or not before calling the log(LogRecord) method. Instead, we just + * provide the few log methods we need here. + */ + +public final class MailLogger { + /** + * For log messages. + */ + private final Logger logger; + /** + * For debug output. + */ + private final String prefix; + /** + * Produce debug output? + */ + private final boolean debug; + /** + * Stream for debug output. + */ + private final PrintStream out; + + /** + * Construct a new MailLogger using the specified Logger name, + * debug prefix (e.g., "DEBUG"), debug flag, and PrintStream. + * + * @param name the Logger name + * @param prefix the prefix for debug output, or null for none + * @param debug if true, write to PrintStream + * @param out the PrintStream to write to + */ + public MailLogger(String name, String prefix, boolean debug, + PrintStream out) { + logger = Logger.getLogger(name); + this.prefix = prefix; + this.debug = debug; + this.out = out != null ? out : System.out; + } + + /** + * Construct a new MailLogger using the specified class' package + * name as the Logger name, + * debug prefix (e.g., "DEBUG"), debug flag, and PrintStream. + * + * @param clazz the Logger name is the package name of this class + * @param prefix the prefix for debug output, or null for none + * @param debug if true, write to PrintStream + * @param out the PrintStream to write to + */ + public MailLogger(Class clazz, String prefix, boolean debug, + PrintStream out) { + String name = packageOf(clazz); + logger = Logger.getLogger(name); + this.prefix = prefix; + this.debug = debug; + this.out = out != null ? out : System.out; + } + + /** + * Construct a new MailLogger using the specified class' package + * name combined with the specified subname as the Logger name, + * debug prefix (e.g., "DEBUG"), debug flag, and PrintStream. + * + * @param clazz the Logger name is the package name of this class + * @param subname the Logger name relative to this Logger name + * @param prefix the prefix for debug output, or null for none + * @param debug if true, write to PrintStream + * @param out the PrintStream to write to + */ + public MailLogger(Class clazz, String subname, String prefix, boolean debug, + PrintStream out) { + String name = packageOf(clazz) + "." + subname; + logger = Logger.getLogger(name); + this.prefix = prefix; + this.debug = debug; + this.out = out != null ? out : System.out; + } + + /** + * Construct a new MailLogger using the specified Logger name and + * debug prefix (e.g., "DEBUG"). Get the debug flag and PrintStream + * from the Session. + * + * @param name the Logger name + * @param prefix the prefix for debug output, or null for none + * @param session where to get the debug flag and PrintStream + */ + @Deprecated + public MailLogger(String name, String prefix, Session session) { + this(name, prefix, session.getDebug(), session.getDebugOut()); + } + + /** + * Construct a new MailLogger using the specified class' package + * name as the Logger name and the specified + * debug prefix (e.g., "DEBUG"). Get the debug flag and PrintStream + * from the Session. + * + * @param clazz the Logger name is the package name of this class + * @param prefix the prefix for debug output, or null for none + * @param session where to get the debug flag and PrintStream + */ + @Deprecated + public MailLogger(Class clazz, String prefix, Session session) { + this(clazz, prefix, session.getDebug(), session.getDebugOut()); + } + + /** + * Create a MailLogger that uses a Logger with the specified name + * and prefix. The new MailLogger uses the same debug flag and + * PrintStream as this MailLogger. + * + * @param name the Logger name + * @param prefix the prefix for debug output, or null for none + * @return a MailLogger for the given name and prefix. + */ + public MailLogger getLogger(String name, String prefix) { + return new MailLogger(name, prefix, debug, out); + } + + /** + * Create a MailLogger using the specified class' package + * name as the Logger name and the specified prefix. + * The new MailLogger uses the same debug flag and + * PrintStream as this MailLogger. + * + * @param clazz the Logger name is the package name of this class + * @param prefix the prefix for debug output, or null for none + * @return a MailLogger for the given name and prefix. + */ + public MailLogger getLogger(Class clazz, String prefix) { + return new MailLogger(clazz, prefix, debug, out); + } + + /** + * Create a MailLogger that uses a Logger whose name is composed + * of this MailLogger's name plus the specified sub-name, separated + * by a dot. The new MailLogger uses the new prefix for debug output. + * This is used primarily by the protocol trace code that wants a + * different prefix (none). + * + * @param subname the Logger name relative to this Logger name + * @param prefix the prefix for debug output, or null for none + * @return a MailLogger for the given name and prefix. + */ + public MailLogger getSubLogger(String subname, String prefix) { + return new MailLogger(logger.getName() + "." + subname, prefix, + debug, out); + } + + /** + * Create a MailLogger that uses a Logger whose name is composed + * of this MailLogger's name plus the specified sub-name, separated + * by a dot. The new MailLogger uses the new prefix for debug output. + * This is used primarily by the protocol trace code that wants a + * different prefix (none). + * + * @param subname the Logger name relative to this Logger name + * @param prefix the prefix for debug output, or null for none + * @param debug the debug flag for the sub-logger + * @return a MailLogger for the given name and prefix. + */ + public MailLogger getSubLogger(String subname, String prefix, + boolean debug) { + return new MailLogger(logger.getName() + "." + subname, prefix, + debug, out); + } + + /** + * Log the message at the specified level. + * @param level the log level. + * @param msg the message. + */ + public void log(Level level, String msg) { + ifDebugOut(msg); + if (logger.isLoggable(level)) { + final StackTraceElement frame = inferCaller(); + logger.logp(level, frame.getClassName(), frame.getMethodName(), msg); + } + } + + /** + * Log the message at the specified level. + * @param level the log level. + * @param msg the message. + * @param param1 the additional parameter. + */ + public void log(Level level, String msg, Object param1) { + if (debug) { + msg = MessageFormat.format(msg, new Object[] { param1 }); + debugOut(msg); + } + + if (logger.isLoggable(level)) { + final StackTraceElement frame = inferCaller(); + logger.logp(level, frame.getClassName(), frame.getMethodName(), msg, param1); + } + } + + /** + * Log the message at the specified level. + * @param level the log level. + * @param msg the message. + * @param params the message parameters. + */ + public void log(Level level, String msg, Object... params) { + if (debug) { + msg = MessageFormat.format(msg, params); + debugOut(msg); + } + + if (logger.isLoggable(level)) { + final StackTraceElement frame = inferCaller(); + logger.logp(level, frame.getClassName(), frame.getMethodName(), msg, params); + } + } + + /** + * Log the message at the specified level using a format string. + * @param level the log level. + * @param msg the message format string. + * @param params the message parameters. + * + * @since JavaMail 1.5.4 + */ + public void logf(Level level, String msg, Object... params) { + msg = String.format(msg, params); + ifDebugOut(msg); + logger.log(level, msg); + } + + /** + * Log the message at the specified level. + * @param level the log level. + * @param msg the message. + * @param thrown the throwable to log. + */ + public void log(Level level, String msg, Throwable thrown) { + if (debug) { + if (thrown != null) { + debugOut(msg + ", THROW: "); + thrown.printStackTrace(out); + } else { + debugOut(msg); + } + } + + if (logger.isLoggable(level)) { + final StackTraceElement frame = inferCaller(); + logger.logp(level, frame.getClassName(), frame.getMethodName(), msg, thrown); + } + } + + /** + * Log a message at the CONFIG level. + * @param msg the message. + */ + public void config(String msg) { + log(Level.CONFIG, msg); + } + + /** + * Log a message at the FINE level. + * @param msg the message. + */ + public void fine(String msg) { + log(Level.FINE, msg); + } + + /** + * Log a message at the FINER level. + * @param msg the message. + */ + public void finer(String msg) { + log(Level.FINER, msg); + } + + /** + * Log a message at the FINEST level. + * @param msg the message. + */ + public void finest(String msg) { + log(Level.FINEST, msg); + } + + /** + * If "debug" is set, or our embedded Logger is loggable at the + * given level, return true. + * @param level the log level. + * @return true if loggable. + */ + public boolean isLoggable(Level level) { + return debug || logger.isLoggable(level); + } + + /** + * Common code to conditionally log debug statements. + * @param msg the message to log. + */ + private void ifDebugOut(String msg) { + if (debug) + debugOut(msg); + } + + /** + * Common formatting for debug output. + * @param msg the message to log. + */ + private void debugOut(String msg) { + if (prefix != null) + out.println(prefix + ": " + msg); + else + out.println(msg); + } + + /** + * Return the package name of the class. + * Sometimes there will be no Package object for the class, + * e.g., if the class loader hasn't created one (see Class.getPackage()). + * @param clazz the class source. + * @return the package name or an empty string. + */ + private String packageOf(Class clazz) { + Package p = clazz.getPackage(); + if (p != null) + return p.getName(); // hopefully the common case + String cname = clazz.getName(); + int i = cname.lastIndexOf('.'); + if (i > 0) + return cname.substring(0, i); + // no package name, now what? + return ""; + } + + /** + * A disadvantage of not being able to use Logger directly in Jakarta Mail + * code is that the "source class" information that Logger guesses will + * always refer to this class instead of our caller. This method + * duplicates what Logger does to try to find *our* caller, so that + * Logger doesn't have to do it (and get the wrong answer), and because + * our caller is what's wanted. + * @return StackTraceElement that logged the message. Treat as read-only. + */ + private StackTraceElement inferCaller() { + // Get the stack trace. + StackTraceElement stack[] = (new Throwable()).getStackTrace(); + // First, search back to a method in the Logger class. + int ix = 0; + while (ix < stack.length) { + StackTraceElement frame = stack[ix]; + String cname = frame.getClassName(); + if (isLoggerImplFrame(cname)) { + break; + } + ix++; + } + // Now search for the first frame before the "Logger" class. + while (ix < stack.length) { + StackTraceElement frame = stack[ix]; + String cname = frame.getClassName(); + if (!isLoggerImplFrame(cname)) { + // We've found the relevant frame. + return frame; + } + ix++; + } + // We haven't found a suitable frame, so just punt. This is + // OK as we are only committed to making a "best effort" here. + return new StackTraceElement(MailLogger.class.getName(), "log", + MailLogger.class.getName(), -1); + } + + /** + * Frames to ignore as part of the MailLogger to JUL bridge. + * @param cname the class name. + * @return true if the class name is part of the MailLogger bridge. + */ + private boolean isLoggerImplFrame(String cname) { + return MailLogger.class.getName().equals(cname); + } +} diff --git a/app/src/main/java/com/sun/mail/util/MailSSLSocketFactory.java b/app/src/main/java/com/sun/mail/util/MailSSLSocketFactory.java new file mode 100644 index 0000000000..2f6363244f --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/MailSSLSocketFactory.java @@ -0,0 +1,356 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; +import java.net.*; +import java.security.*; +import java.security.cert.*; +import java.util.*; + +import javax.net.ssl.*; + +/** + * An SSL socket factory that makes it easier to specify trust. + * This socket factory can be configured to trust all hosts or + * trust a specific set of hosts, in which case the server's + * certificate isn't verified. Alternatively, a custom TrustManager + * can be supplied.

+ * + * An instance of this factory can be set as the value of the + * mail.<protocol>.ssl.socketFactory property. + * + * @since JavaMail 1.4.2 + * @author Stephan Sann + * @author Bill Shannon + */ +public class MailSSLSocketFactory extends SSLSocketFactory { + + /** Should all hosts be trusted? */ + private boolean trustAllHosts; + + /** String-array of trusted hosts */ + private String[] trustedHosts = null; + + /** Holds a SSLContext to get SSLSocketFactories from */ + private SSLContext sslcontext; + + /** Holds the KeyManager array to use */ + private KeyManager[] keyManagers; + + /** Holds the TrustManager array to use */ + private TrustManager[] trustManagers; + + /** Holds the SecureRandom to use */ + private SecureRandom secureRandom; + + /** Holds a SSLSocketFactory to pass all API-method-calls to */ + private SSLSocketFactory adapteeFactory = null; + + /** + * Initializes a new MailSSLSocketFactory. + * + * @throws GeneralSecurityException for security errors + */ + public MailSSLSocketFactory() throws GeneralSecurityException { + this("TLS"); + } + + /** + * Initializes a new MailSSLSocketFactory with a given protocol. + * Normally the protocol will be specified as "TLS". + * + * @param protocol The protocol to use + * @throws NoSuchAlgorithmException if given protocol is not supported + * @throws GeneralSecurityException for security errors + */ + public MailSSLSocketFactory(String protocol) + throws GeneralSecurityException { + + // By default we do NOT trust all hosts. + trustAllHosts = false; + + // Get an instance of an SSLContext. + sslcontext = SSLContext.getInstance(protocol); + + // Default properties to init the SSLContext + keyManagers = null; + trustManagers = new TrustManager[] { new MailTrustManager() }; + secureRandom = null; + + // Assemble a default SSLSocketFactory to delegate all API-calls to. + newAdapteeFactory(); + } + + + /** + * Gets an SSLSocketFactory based on the given (or default) + * KeyManager array, TrustManager array and SecureRandom and + * sets it to the instance var adapteeFactory. + * + * @throws KeyManagementException for key manager errors + */ + private synchronized void newAdapteeFactory() + throws KeyManagementException { + sslcontext.init(keyManagers, trustManagers, secureRandom); + + // Get SocketFactory and save it in our instance var + adapteeFactory = sslcontext.getSocketFactory(); + } + + /** + * @return the keyManagers + */ + public synchronized KeyManager[] getKeyManagers() { + return keyManagers.clone(); + } + + /** + * @param keyManagers the keyManagers to set + * @throws GeneralSecurityException for security errors + */ + public synchronized void setKeyManagers(KeyManager... keyManagers) + throws GeneralSecurityException { + this.keyManagers = keyManagers.clone(); + newAdapteeFactory(); + } + + /** + * @return the secureRandom + */ + public synchronized SecureRandom getSecureRandom() { + return secureRandom; + } + + /** + * @param secureRandom the secureRandom to set + * @throws GeneralSecurityException for security errors + */ + public synchronized void setSecureRandom(SecureRandom secureRandom) + throws GeneralSecurityException { + this.secureRandom = secureRandom; + newAdapteeFactory(); + } + + /** + * @return the trustManagers + */ + public synchronized TrustManager[] getTrustManagers() { + return trustManagers; + } + + /** + * @param trustManagers the trustManagers to set + * @throws GeneralSecurityException for security errors + */ + public synchronized void setTrustManagers(TrustManager... trustManagers) + throws GeneralSecurityException { + this.trustManagers = trustManagers; + newAdapteeFactory(); + } + + /** + * @return true if all hosts should be trusted + */ + public synchronized boolean isTrustAllHosts() { + return trustAllHosts; + } + + /** + * @param trustAllHosts should all hosts be trusted? + */ + public synchronized void setTrustAllHosts(boolean trustAllHosts) { + this.trustAllHosts = trustAllHosts; + } + + /** + * @return the trusted hosts + */ + public synchronized String[] getTrustedHosts() { + if (trustedHosts == null) + return null; + else + return trustedHosts.clone(); + } + + /** + * @param trustedHosts the hosts to trust + */ + public synchronized void setTrustedHosts(String... trustedHosts) { + if (trustedHosts == null) + this.trustedHosts = null; + else + this.trustedHosts = trustedHosts.clone(); + } + + /** + * After a successful conection to the server, this method is + * called to ensure that the server should be trusted. + * + * @param server name of the server we connected to + * @param sslSocket SSLSocket connected to the server + * @return true if "trustAllHosts" is set to true OR the server + * is contained in the "trustedHosts" array; + */ + public synchronized boolean isServerTrusted(String server, + SSLSocket sslSocket) { + + //System.out.println("DEBUG: isServerTrusted host " + server); + + // If "trustAllHosts" is set to true, we return true + if (trustAllHosts) + return true; + + // If the socket host is contained in the "trustedHosts" array, + // we return true + if (trustedHosts != null) + return Arrays.asList(trustedHosts).contains(server); // ignore case? + + // If we get here, trust of the server was verified by the trust manager + return true; + } + + + // SocketFactory methods + + /* (non-Javadoc) + * @see javax.net.ssl.SSLSocketFactory#createSocket(java.net.Socket, + * java.lang.String, int, boolean) + */ + @Override + public synchronized Socket createSocket(Socket socket, String s, int i, + boolean flag) throws IOException { + return adapteeFactory.createSocket(socket, s, i, flag); + } + + /* (non-Javadoc) + * @see javax.net.ssl.SSLSocketFactory#getDefaultCipherSuites() + */ + @Override + public synchronized String[] getDefaultCipherSuites() { + return adapteeFactory.getDefaultCipherSuites(); + } + + /* (non-Javadoc) + * @see javax.net.ssl.SSLSocketFactory#getSupportedCipherSuites() + */ + @Override + public synchronized String[] getSupportedCipherSuites() { + return adapteeFactory.getSupportedCipherSuites(); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket() + */ + @Override + public synchronized Socket createSocket() throws IOException { + return adapteeFactory.createSocket(); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.net.InetAddress, int, + * java.net.InetAddress, int) + */ + @Override + public synchronized Socket createSocket(InetAddress inetaddress, int i, + InetAddress inetaddress1, int j) throws IOException { + return adapteeFactory.createSocket(inetaddress, i, inetaddress1, j); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.net.InetAddress, int) + */ + @Override + public synchronized Socket createSocket(InetAddress inetaddress, int i) + throws IOException { + return adapteeFactory.createSocket(inetaddress, i); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.lang.String, int, + * java.net.InetAddress, int) + */ + @Override + public synchronized Socket createSocket(String s, int i, + InetAddress inetaddress, int j) + throws IOException, UnknownHostException { + return adapteeFactory.createSocket(s, i, inetaddress, j); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.lang.String, int) + */ + @Override + public synchronized Socket createSocket(String s, int i) + throws IOException, UnknownHostException { + return adapteeFactory.createSocket(s, i); + } + + + // inner classes + + /** + * A default Trustmanager. + * + * @author Stephan Sann + */ + private class MailTrustManager implements X509TrustManager { + + /** A TrustManager to pass method calls to */ + private X509TrustManager adapteeTrustManager = null; + + /** + * Initializes a new TrustManager instance. + */ + private MailTrustManager() throws GeneralSecurityException { + TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); + tmf.init((KeyStore)null); + adapteeTrustManager = (X509TrustManager)tmf.getTrustManagers()[0]; + } + + /* (non-Javadoc) + * @see javax.net.ssl.X509TrustManager#checkClientTrusted( + * java.security.cert.X509Certificate[], java.lang.String) + */ + @Override + public void checkClientTrusted(X509Certificate[] certs, String authType) + throws CertificateException { + if (!(isTrustAllHosts() || getTrustedHosts() != null)) + adapteeTrustManager.checkClientTrusted(certs, authType); + } + + /* (non-Javadoc) + * @see javax.net.ssl.X509TrustManager#checkServerTrusted( + * java.security.cert.X509Certificate[], java.lang.String) + */ + @Override + public void checkServerTrusted(X509Certificate[] certs, String authType) + throws CertificateException { + + if (!(isTrustAllHosts() || getTrustedHosts() != null)) + adapteeTrustManager.checkServerTrusted(certs, authType); + } + + /* (non-Javadoc) + * @see javax.net.ssl.X509TrustManager#getAcceptedIssuers() + */ + @Override + public X509Certificate[] getAcceptedIssuers() { + return adapteeTrustManager.getAcceptedIssuers(); + } + } +} diff --git a/app/src/main/java/com/sun/mail/util/MessageRemovedIOException.java b/app/src/main/java/com/sun/mail/util/MessageRemovedIOException.java new file mode 100644 index 0000000000..96840f773f --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/MessageRemovedIOException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.IOException; + +/** + * A variant of MessageRemovedException that can be thrown from methods + * that only throw IOException. The getContent method will catch this + * exception and translate it back to MessageRemovedException. + * + * @see javax.mail.Message#isExpunged() + * @see javax.mail.Message#getMessageNumber() + * @author Bill Shannon + */ + +public class MessageRemovedIOException extends IOException { + + private static final long serialVersionUID = 4280468026581616424L; + + /** + * Constructs a MessageRemovedIOException with no detail message. + */ + public MessageRemovedIOException() { + super(); + } + + /** + * Constructs a MessageRemovedIOException with the specified detail message. + * @param s the detail message + */ + public MessageRemovedIOException(String s) { + super(s); + } +} diff --git a/app/src/main/java/com/sun/mail/util/MimeUtil.java b/app/src/main/java/com/sun/mail/util/MimeUtil.java new file mode 100644 index 0000000000..6944249857 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/MimeUtil.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.lang.reflect.*; +import java.security.*; + +import javax.mail.internet.MimePart; + +/** + * General MIME-related utility methods. + * + * @author Bill Shannon + * @since JavaMail 1.4.4 + */ +public class MimeUtil { + + private static final Method cleanContentType; + + static { + Method meth = null; + try { + String cth = System.getProperty("mail.mime.contenttypehandler"); + if (cth != null) { + ClassLoader cl = getContextClassLoader(); + Class clsHandler = null; + if (cl != null) { + try { + clsHandler = Class.forName(cth, false, cl); + } catch (ClassNotFoundException cex) { } + } + if (clsHandler == null) + clsHandler = Class.forName(cth); + meth = clsHandler.getMethod("cleanContentType", + new Class[] { MimePart.class, String.class }); + } + } catch (ClassNotFoundException ex) { + // ignore it + } catch (NoSuchMethodException ex) { + // ignore it + } catch (RuntimeException ex) { + // ignore it + } finally { + cleanContentType = meth; + } + } + + // No one should instantiate this class. + private MimeUtil() { + } + + /** + * If a Content-Type handler has been specified, + * call it to clean up the Content-Type value. + * + * @param mp the MimePart + * @param contentType the Content-Type value + * @return the cleaned Content-Type value + */ + public static String cleanContentType(MimePart mp, String contentType) { + if (cleanContentType != null) { + try { + return (String)cleanContentType.invoke(null, + new Object[] { mp, contentType }); + } catch (Exception ex) { + return contentType; + } + } else + return contentType; + } + + /** + * Convenience method to get our context class loader. + * Assert any privileges we might have and then call the + * Thread.getContextClassLoader method. + */ + private static ClassLoader getContextClassLoader() { + return + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public ClassLoader run() { + ClassLoader cl = null; + try { + cl = Thread.currentThread().getContextClassLoader(); + } catch (SecurityException ex) { } + return cl; + } + }); + } +} diff --git a/app/src/main/java/com/sun/mail/util/PropUtil.java b/app/src/main/java/com/sun/mail/util/PropUtil.java new file mode 100644 index 0000000000..430b378692 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/PropUtil.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.util.*; +import javax.mail.Session; + +/** + * Utilities to make it easier to get property values. + * Properties can be strings or type-specific value objects. + * + * @author Bill Shannon + */ +public class PropUtil { + + // No one should instantiate this class. + private PropUtil() { + } + + /** + * Get an integer valued property. + * + * @param props the properties + * @param name the property name + * @param def default value if property not found + * @return the property value + */ + public static int getIntProperty(Properties props, String name, int def) { + return getInt(getProp(props, name), def); + } + + /** + * Get a boolean valued property. + * + * @param props the properties + * @param name the property name + * @param def default value if property not found + * @return the property value + */ + public static boolean getBooleanProperty(Properties props, + String name, boolean def) { + return getBoolean(getProp(props, name), def); + } + + /** + * Get an integer valued property. + * + * @param session the Session + * @param name the property name + * @param def default value if property not found + * @return the property value + */ + @Deprecated + public static int getIntSessionProperty(Session session, + String name, int def) { + return getInt(getProp(session.getProperties(), name), def); + } + + /** + * Get a boolean valued property. + * + * @param session the Session + * @param name the property name + * @param def default value if property not found + * @return the property value + */ + @Deprecated + public static boolean getBooleanSessionProperty(Session session, + String name, boolean def) { + return getBoolean(getProp(session.getProperties(), name), def); + } + + /** + * Get a boolean valued System property. + * + * @param name the property name + * @param def default value if property not found + * @return the property value + */ + public static boolean getBooleanSystemProperty(String name, boolean def) { + try { + return getBoolean(getProp(System.getProperties(), name), def); + } catch (SecurityException sex) { + // fall through... + } + + /* + * If we can't get the entire System Properties object because + * of a SecurityException, just ask for the specific property. + */ + try { + String value = System.getProperty(name); + if (value == null) + return def; + if (def) + return !value.equalsIgnoreCase("false"); + else + return value.equalsIgnoreCase("true"); + } catch (SecurityException sex) { + return def; + } + } + + /** + * Get the value of the specified property. + * If the "get" method returns null, use the getProperty method, + * which might cascade to a default Properties object. + */ + private static Object getProp(Properties props, String name) { + Object val = props.get(name); + if (val != null) + return val; + else + return props.getProperty(name); + } + + /** + * Interpret the value object as an integer, + * returning def if unable. + */ + private static int getInt(Object value, int def) { + if (value == null) + return def; + if (value instanceof String) { + try { + String s = (String)value; + if (s.startsWith("0x")) + return Integer.parseInt(s.substring(2), 16); + else + return Integer.parseInt(s); + } catch (NumberFormatException nfex) { } + } + if (value instanceof Integer) + return ((Integer)value).intValue(); + return def; + } + + /** + * Interpret the value object as a boolean, + * returning def if unable. + */ + private static boolean getBoolean(Object value, boolean def) { + if (value == null) + return def; + if (value instanceof String) { + /* + * If the default is true, only "false" turns it off. + * If the default is false, only "true" turns it on. + */ + if (def) + return !((String)value).equalsIgnoreCase("false"); + else + return ((String)value).equalsIgnoreCase("true"); + } + if (value instanceof Boolean) + return ((Boolean)value).booleanValue(); + return def; + } +} diff --git a/app/src/main/java/com/sun/mail/util/QDecoderStream.java b/app/src/main/java/com/sun/mail/util/QDecoderStream.java new file mode 100644 index 0000000000..19ea0c6785 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/QDecoderStream.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a Q Decoder as defined in RFC 2047 + * for decoding MIME headers. It subclasses the QPDecoderStream class. + * + * @author John Mani + */ + +public class QDecoderStream extends QPDecoderStream { + + /** + * Create a Q-decoder that decodes the specified input stream. + * @param in the input stream + */ + public QDecoderStream(InputStream in) { + super(in); + } + + /** + * Read the next decoded byte from this input stream. The byte + * is returned as an int in the range 0 + * to 255. If no byte is available because the end of + * the stream has been reached, the value -1 is returned. + * This method blocks until input data is available, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next byte of data, or -1 if the end of the + * stream is reached. + * @exception IOException if an I/O error occurs. + */ + @Override + public int read() throws IOException { + int c = in.read(); + + if (c == '_') // Return '_' as ' ' + return ' '; + else if (c == '=') { + // QP Encoded atom. Get the next two bytes .. + ba[0] = (byte)in.read(); + ba[1] = (byte)in.read(); + // .. and decode them + try { + return ASCIIUtility.parseInt(ba, 0, 2, 16); + } catch (NumberFormatException nex) { + throw new DecodingException( + "QDecoder: Error in QP stream " + nex.getMessage()); + } + } else + return c; + } +} diff --git a/app/src/main/java/com/sun/mail/util/QEncoderStream.java b/app/src/main/java/com/sun/mail/util/QEncoderStream.java new file mode 100644 index 0000000000..659897aec3 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/QEncoderStream.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a Q Encoder as defined by RFC 2047 for + * encoding MIME headers. It subclasses the QPEncoderStream class. + * + * @author John Mani + */ + +public class QEncoderStream extends QPEncoderStream { + + private String specials; + private static String WORD_SPECIALS = "=_?\"#$%&'(),.:;<>@[\\]^`{|}~"; + private static String TEXT_SPECIALS = "=_?"; + + /** + * Create a Q encoder that encodes the specified input stream + * @param out the output stream + * @param encodingWord true if we are Q-encoding a word within a + * phrase. + */ + public QEncoderStream(OutputStream out, boolean encodingWord) { + super(out, Integer.MAX_VALUE); // MAX_VALUE is 2^31, should + // suffice (!) to indicate that + // CRLFs should not be inserted + // when encoding rfc822 headers + + // a RFC822 "word" token has more restrictions than a + // RFC822 "text" token. + specials = encodingWord ? WORD_SPECIALS : TEXT_SPECIALS; + } + + /** + * Encodes the specified byte to this output stream. + * @param c the byte. + * @exception IOException if an I/O error occurs. + */ + @Override + public void write(int c) throws IOException { + c = c & 0xff; // Turn off the MSB. + if (c == ' ') + output('_', false); + else if (c < 040 || c >= 0177 || specials.indexOf(c) >= 0) + // Encoding required. + output(c, true); + else // No encoding required + output(c, false); + } + + /** + * Returns the length of the encoded version of this byte array. + * + * @param b the byte array + * @param encodingWord true if encoding words, false if encoding text + * @return the length + */ + public static int encodedLength(byte[] b, boolean encodingWord) { + int len = 0; + String specials = encodingWord ? WORD_SPECIALS: TEXT_SPECIALS; + for (int i = 0; i < b.length; i++) { + int c = b[i] & 0xff; // Mask off MSB + if (c < 040 || c >= 0177 || specials.indexOf(c) >= 0) + // needs encoding + len += 3; // Q-encoding is 1 -> 3 conversion + else + len++; + } + return len; + } + + /**** begin TEST program *** + public static void main(String argv[]) throws Exception { + FileInputStream infile = new FileInputStream(argv[0]); + QEncoderStream encoder = new QEncoderStream(System.out); + int c; + + while ((c = infile.read()) != -1) + encoder.write(c); + encoder.close(); + } + *** end TEST program ***/ +} diff --git a/app/src/main/java/com/sun/mail/util/QPDecoderStream.java b/app/src/main/java/com/sun/mail/util/QPDecoderStream.java new file mode 100644 index 0000000000..284c6145ca --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/QPDecoderStream.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a QP Decoder. It is implemented as + * a FilterInputStream, so one can just wrap this class around + * any input stream and read bytes from this filter. The decoding + * is done as the bytes are read out. + * + * @author John Mani + */ + +public class QPDecoderStream extends FilterInputStream { + protected byte[] ba = new byte[2]; + protected int spaces = 0; + + /** + * Create a Quoted Printable decoder that decodes the specified + * input stream. + * @param in the input stream + */ + public QPDecoderStream(InputStream in) { + super(new PushbackInputStream(in, 2)); // pushback of size=2 + } + + /** + * Read the next decoded byte from this input stream. The byte + * is returned as an int in the range 0 + * to 255. If no byte is available because the end of + * the stream has been reached, the value -1 is returned. + * This method blocks until input data is available, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next byte of data, or -1 if the end of the + * stream is reached. + * @exception IOException if an I/O error occurs. + */ + @Override + public int read() throws IOException { + if (spaces > 0) { + // We have cached space characters, return one + spaces--; + return ' '; + } + + int c = in.read(); + + if (c == ' ') { + // Got space, keep reading till we get a non-space char + while ((c = in.read()) == ' ') + spaces++; + + if (c == '\r' || c == '\n' || c == -1) + // If the non-space char is CR/LF/EOF, the spaces we got + // so far is junk introduced during transport. Junk 'em. + spaces = 0; + else { + // The non-space char is NOT CR/LF, the spaces are valid. + ((PushbackInputStream)in).unread(c); + c = ' '; + } + return c; // return either or + } + else if (c == '=') { + // QP Encoded atom. Decode the next two bytes + int a = in.read(); + + if (a == '\n') { + /* Hmm ... not really confirming QP encoding, but lets + * allow this as a LF terminated encoded line .. and + * consider this a soft linebreak and recurse to fetch + * the next char. + */ + return read(); + } else if (a == '\r') { + // Expecting LF. This forms a soft linebreak to be ignored. + int b = in.read(); + if (b != '\n') + /* Not really confirming QP encoding, but + * lets allow this as well. + */ + ((PushbackInputStream)in).unread(b); + return read(); + } else if (a == -1) { + // Not valid QP encoding, but we be nice and tolerant here ! + return -1; + } else { + ba[0] = (byte)a; + ba[1] = (byte)in.read(); + try { + return ASCIIUtility.parseInt(ba, 0, 2, 16); + } catch (NumberFormatException nex) { + /* + System.err.println( + "Illegal characters in QP encoded stream: " + + ASCIIUtility.toString(ba, 0, 2) + ); + */ + + ((PushbackInputStream)in).unread(ba); + return c; + } + } + } + return c; + } + + /** + * Reads up to len decoded bytes of data from this input stream + * into an array of bytes. This method blocks until some input is + * available. + *

+ * + * @param buf the buffer into which the data is read. + * @param off the start offset of the data. + * @param len the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of + * the stream has been reached. + * @exception IOException if an I/O error occurs. + */ + @Override + public int read(byte[] buf, int off, int len) throws IOException { + int i, c; + for (i = 0; i < len; i++) { + if ((c = read()) == -1) { + if (i == 0) // At end of stream, so we should + i = -1; // return -1 , NOT 0. + break; + } + buf[off+i] = (byte)c; + } + return i; + } + + /** + * Skips over and discards n bytes of data from this stream. + */ + @Override + public long skip(long n) throws IOException { + long skipped = 0; + while (n-- > 0 && read() >= 0) + skipped++; + return skipped; + } + + /** + * Tests if this input stream supports marks. Currently this class + * does not support marks + */ + @Override + public boolean markSupported() { + return false; + } + + /** + * Returns the number of bytes that can be read from this input + * stream without blocking. The QP algorithm does not permit + * a priori knowledge of the number of bytes after decoding, so + * this method just invokes the available method + * of the original input stream. + */ + @Override + public int available() throws IOException { + // This is bogus ! We don't really know how much + // bytes are available *after* decoding + return in.available(); + } + + /**** begin TEST program + public static void main(String argv[]) throws Exception { + FileInputStream infile = new FileInputStream(argv[0]); + QPDecoderStream decoder = new QPDecoderStream(infile); + int c; + + while ((c = decoder.read()) != -1) + System.out.print((char)c); + System.out.println(); + } + *** end TEST program ****/ +} diff --git a/app/src/main/java/com/sun/mail/util/QPEncoderStream.java b/app/src/main/java/com/sun/mail/util/QPEncoderStream.java new file mode 100644 index 0000000000..d39102dae6 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/QPEncoderStream.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a Quoted Printable Encoder. It is implemented as + * a FilterOutputStream, so one can just wrap this class around + * any output stream and write bytes into this filter. The Encoding + * is done as the bytes are written out. + * + * @author John Mani + */ + +public class QPEncoderStream extends FilterOutputStream { + private int count = 0; // number of bytes that have been output + private int bytesPerLine; // number of bytes per line + private boolean gotSpace = false; + private boolean gotCR = false; + + /** + * Create a QP encoder that encodes the specified input stream + * @param out the output stream + * @param bytesPerLine the number of bytes per line. The encoder + * inserts a CRLF sequence after this many number + * of bytes. + */ + public QPEncoderStream(OutputStream out, int bytesPerLine) { + super(out); + // Subtract 1 to account for the '=' in the soft-return + // at the end of a line + this.bytesPerLine = bytesPerLine - 1; + } + + /** + * Create a QP encoder that encodes the specified input stream. + * Inserts the CRLF sequence after outputting 76 bytes. + * @param out the output stream + */ + public QPEncoderStream(OutputStream out) { + this(out, 76); + } + + /** + * Encodes len bytes from the specified + * byte array starting at offset off to + * this output stream. + * + * @param b the data. + * @param off the start offset in the data. + * @param len the number of bytes to write. + * @exception IOException if an I/O error occurs. + */ + @Override + public void write(byte[] b, int off, int len) throws IOException { + for (int i = 0; i < len; i++) + write(b[off + i]); + } + + /** + * Encodes b.length bytes to this output stream. + * @param b the data to be written. + * @exception IOException if an I/O error occurs. + */ + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + /** + * Encodes the specified byte to this output stream. + * @param c the byte. + * @exception IOException if an I/O error occurs. + */ + @Override + public void write(int c) throws IOException { + c = c & 0xff; // Turn off the MSB. + if (gotSpace) { // previous character was + if (c == '\r' || c == '\n') + // if CR/LF, we need to encode the char + output(' ', true); + else // no encoding required, just output the char + output(' ', false); + gotSpace = false; + } + + if (c == '\r') { + gotCR = true; + outputCRLF(); + } else { + if (c == '\n') { + if (gotCR) + // This is a CRLF sequence, we already output the + // corresponding CRLF when we got the CR, so ignore this + ; + else + outputCRLF(); + } else if (c == ' ') { + gotSpace = true; + } else if (c < 040 || c >= 0177 || c == '=') + // Encoding required. + output(c, true); + else // No encoding required + output(c, false); + // whatever it was, it wasn't a CR + gotCR = false; + } + } + + /** + * Flushes this output stream and forces any buffered output bytes + * to be encoded out to the stream. + * @exception IOException if an I/O error occurs. + */ + @Override + public void flush() throws IOException { + if (gotSpace) { + output(' ', true); + gotSpace = false; + } + out.flush(); + } + + /** + * Forces any buffered output bytes to be encoded out to the stream + * and closes this output stream. + * + * @exception IOException for I/O errors + */ + @Override + public void close() throws IOException { + flush(); + out.close(); + } + + private void outputCRLF() throws IOException { + out.write('\r'); + out.write('\n'); + count = 0; + } + + // The encoding table + private final static char hex[] = { + '0','1', '2', '3', '4', '5', '6', '7', + '8','9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + + protected void output(int c, boolean encode) throws IOException { + if (encode) { + if ((count += 3) > bytesPerLine) { + out.write('='); + out.write('\r'); + out.write('\n'); + count = 3; // set the next line's length + } + out.write('='); + out.write(hex[c >> 4]); + out.write(hex[c & 0xf]); + } else { + if (++count > bytesPerLine) { + out.write('='); + out.write('\r'); + out.write('\n'); + count = 1; // set the next line's length + } + out.write(c); + } + } + + /**** begin TEST program *** + public static void main(String argv[]) throws Exception { + FileInputStream infile = new FileInputStream(argv[0]); + QPEncoderStream encoder = new QPEncoderStream(System.out); + int c; + + while ((c = infile.read()) != -1) + encoder.write(c); + encoder.close(); + } + *** end TEST program ***/ +} diff --git a/app/src/main/java/com/sun/mail/util/ReadableMime.java b/app/src/main/java/com/sun/mail/util/ReadableMime.java new file mode 100644 index 0000000000..ecba7e2a49 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/ReadableMime.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.InputStream; + +import javax.mail.MessagingException; + +/** + * A Message or message Part whose data can be read as a MIME format + * stream. Note that the MIME stream will include both the headers + * and the body of the message or part. This should be the same data + * that is produced by the writeTo method, but in a readable form. + * + * @author Bill Shannon + * @since JavaMail 1.4.5 + */ +public interface ReadableMime { + /** + * Return the MIME format stream corresponding to this message part. + * + * @return the MIME format stream + * @exception MessagingException for failures + */ + public InputStream getMimeStream() throws MessagingException; +} diff --git a/app/src/main/java/com/sun/mail/util/SharedByteArrayOutputStream.java b/app/src/main/java/com/sun/mail/util/SharedByteArrayOutputStream.java new file mode 100644 index 0000000000..9fd617ea9b --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/SharedByteArrayOutputStream.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.InputStream; +import java.io.ByteArrayOutputStream; + +import javax.mail.util.SharedByteArrayInputStream; + +/** + * A ByteArrayOutputStream that allows us to share the byte array + * rather than copy it. Eventually could replace this with something + * that doesn't require a single contiguous byte array. + * + * @author Bill Shannon + * @since JavaMail 1.4.5 + */ +public class SharedByteArrayOutputStream extends ByteArrayOutputStream { + public SharedByteArrayOutputStream(int size) { + super(size); + } + + public InputStream toStream() { + return new SharedByteArrayInputStream(buf, 0, count); + } +} diff --git a/app/src/main/java/com/sun/mail/util/SocketConnectException.java b/app/src/main/java/com/sun/mail/util/SocketConnectException.java new file mode 100644 index 0000000000..6b3c16e082 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/SocketConnectException.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.IOException; + +/** + * An IOException that indicates a socket connection attempt failed. + * Unlike java.net.ConnectException, it includes details of what we + * were trying to connect to. + * + * @see java.net.ConnectException + * @author Bill Shannon + * @since JavaMail 1.5.0 + */ + +public class SocketConnectException extends IOException { + /** + * The socket host name. + */ + private String host; + /** + * The socket port. + */ + private int port; + /** + * The connection timeout. + */ + private int cto; + /** + * The generated serial id. + */ + private static final long serialVersionUID = 3997871560538755463L; + + /** + * Constructs a SocketConnectException. + * + * @param msg error message detail + * @param cause the underlying exception that indicates the failure + * @param host the host we were trying to connect to + * @param port the port we were trying to connect to + * @param cto the timeout for the connection attempt + */ + public SocketConnectException(String msg, Exception cause, + String host, int port, int cto) { + super(msg); + initCause(cause); + this.host = host; + this.port = port; + this.cto = cto; + } + + /** + * The exception that caused the failure. + * + * @return the exception + */ + public Exception getException() { + // the "cause" is always an Exception; see constructor above + Throwable t = getCause(); + assert t == null || t instanceof Exception; + return (Exception) t; + } + + /** + * The host we were trying to connect to. + * + * @return the host + */ + public String getHost() { + return host; + } + + /** + * The port we were trying to connect to. + * + * @return the port + */ + public int getPort() { + return port; + } + + /** + * The timeout used for the connection attempt. + * + * @return the connection timeout + */ + public int getConnectionTimeout() { + return cto; + } +} diff --git a/app/src/main/java/com/sun/mail/util/SocketFetcher.java b/app/src/main/java/com/sun/mail/util/SocketFetcher.java new file mode 100644 index 0000000000..d6af220615 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/SocketFetcher.java @@ -0,0 +1,895 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.security.*; +import java.net.*; +import java.io.*; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; +import java.lang.reflect.*; +import java.util.*; +import java.util.regex.*; +import java.util.logging.Level; +import java.security.cert.*; +import javax.net.*; +import javax.net.ssl.*; + +/** + * This class is used to get Sockets. Depending on the arguments passed + * it will either return a plain java.net.Socket or dynamically load + * the SocketFactory class specified in the classname param and return + * a socket created by that SocketFactory. + * + * @author Max Spivak + * @author Bill Shannon + */ +public class SocketFetcher { + + private static MailLogger logger = new MailLogger( + SocketFetcher.class, + "socket", + "DEBUG SocketFetcher", + PropUtil.getBooleanSystemProperty("mail.socket.debug", false), + System.out); + + // No one should instantiate this class. + private SocketFetcher() { + } + + /** + * This method returns a Socket. Properties control the use of + * socket factories and other socket characteristics. The properties + * used are: + *

    + *
  • prefix.socketFactory + *
  • prefix.socketFactory.class + *
  • prefix.socketFactory.fallback + *
  • prefix.socketFactory.port + *
  • prefix.ssl.socketFactory + *
  • prefix.ssl.socketFactory.class + *
  • prefix.ssl.socketFactory.port + *
  • prefix.timeout + *
  • prefix.connectiontimeout + *
  • prefix.localaddress + *
  • prefix.localport + *
  • prefix.usesocketchannels + *

+ * If we're making an SSL connection, the ssl.socketFactory + * properties are used first, if set.

+ * + * If the socketFactory property is set, the value is an + * instance of a SocketFactory class, not a string. The + * instance is used directly. If the socketFactory property + * is not set, the socketFactory.class property is considered. + * (Note that the SocketFactory property must be set using the + * put method, not the setProperty + * method.)

+ * + * If the socketFactory.class property isn't set, the socket + * returned is an instance of java.net.Socket connected to the + * given host and port. If the socketFactory.class property is set, + * it is expected to contain a fully qualified classname of a + * javax.net.SocketFactory subclass. In this case, the class is + * dynamically instantiated and a socket created by that + * SocketFactory is returned.

+ * + * If the socketFactory.fallback property is set to false, don't + * fall back to using regular sockets if the socket factory fails.

+ * + * The socketFactory.port specifies a port to use when connecting + * through the socket factory. If unset, the port argument will be + * used.

+ * + * If the connectiontimeout property is set, the timeout is passed + * to the socket connect method.

+ * + * If the timeout property is set, it is used to set the socket timeout. + *

+ * + * If the localaddress property is set, it's used as the local address + * to bind to. If the localport property is also set, it's used as the + * local port number to bind to.

+ * + * If the usesocketchannels property is set, and we create the Socket + * ourself, and the selection of other properties allows, create a + * SocketChannel and get the Socket from it. This allows us to later + * retrieve the SocketChannel from the Socket and use it with Select. + * + * @param host The host to connect to + * @param port The port to connect to at the host + * @param props Properties object containing socket properties + * @param prefix Property name prefix, e.g., "mail.imap" + * @param useSSL use the SSL socket factory as the default + * @return the Socket + * @exception IOException for I/O errors + */ + public static Socket getSocket(String host, int port, Properties props, + String prefix, boolean useSSL) + throws IOException { + + if (logger.isLoggable(Level.FINER)) + logger.finer("getSocket" + ", host " + host + ", port " + port + + ", prefix " + prefix + ", useSSL " + useSSL); + if (prefix == null) + prefix = "socket"; + if (props == null) + props = new Properties(); // empty + int cto = PropUtil.getIntProperty(props, + prefix + ".connectiontimeout", -1); + Socket socket = null; + String localaddrstr = props.getProperty(prefix + ".localaddress", null); + InetAddress localaddr = null; + if (localaddrstr != null) + localaddr = InetAddress.getByName(localaddrstr); + int localport = PropUtil.getIntProperty(props, + prefix + ".localport", 0); + + boolean fb = PropUtil.getBooleanProperty(props, + prefix + ".socketFactory.fallback", true); + + int sfPort = -1; + String sfErr = "unknown socket factory"; + int to = PropUtil.getIntProperty(props, prefix + ".timeout", -1); + try { + /* + * If using SSL, first look for SSL-specific class name or + * factory instance. + */ + SocketFactory sf = null; + String sfPortName = null; + if (useSSL) { + Object sfo = props.get(prefix + ".ssl.socketFactory"); + if (sfo instanceof SocketFactory) { + sf = (SocketFactory)sfo; + sfErr = "SSL socket factory instance " + sf; + } + if (sf == null) { + String sfClass = + props.getProperty(prefix + ".ssl.socketFactory.class"); + sf = getSocketFactory(sfClass); + sfErr = "SSL socket factory class " + sfClass; + } + sfPortName = ".ssl.socketFactory.port"; + } + + if (sf == null) { + Object sfo = props.get(prefix + ".socketFactory"); + if (sfo instanceof SocketFactory) { + sf = (SocketFactory)sfo; + sfErr = "socket factory instance " + sf; + } + if (sf == null) { + String sfClass = + props.getProperty(prefix + ".socketFactory.class"); + sf = getSocketFactory(sfClass); + sfErr = "socket factory class " + sfClass; + } + sfPortName = ".socketFactory.port"; + } + + // if we now have a socket factory, use it + if (sf != null) { + sfPort = PropUtil.getIntProperty(props, + prefix + sfPortName, -1); + + // if port passed in via property isn't valid, use param + if (sfPort == -1) + sfPort = port; + socket = createSocket(localaddr, localport, + host, sfPort, cto, to, props, prefix, sf, useSSL); + } + } catch (SocketTimeoutException sex) { + throw sex; + } catch (Exception ex) { + if (!fb) { + if (ex instanceof InvocationTargetException) { + Throwable t = + ((InvocationTargetException)ex).getTargetException(); + if (t instanceof Exception) + ex = (Exception)t; + } + if (ex instanceof IOException) + throw (IOException)ex; + throw new SocketConnectException("Using " + sfErr, ex, + host, sfPort, cto); + } + } + + if (socket == null) { + socket = createSocket(localaddr, localport, + host, port, cto, to, props, prefix, null, useSSL); + + } else { + if (to >= 0) { + if (logger.isLoggable(Level.FINEST)) + logger.finest("set socket read timeout " + to); + socket.setSoTimeout(to); + } + } + + return socket; + } + + public static Socket getSocket(String host, int port, Properties props, + String prefix) throws IOException { + return getSocket(host, port, props, prefix, false); + } + + /** + * Create a socket with the given local address and connected to + * the given host and port. Use the specified connection timeout + * and read timeout. + * If a socket factory is specified, use it. Otherwise, use the + * SSLSocketFactory if useSSL is true. + */ + private static Socket createSocket(InetAddress localaddr, int localport, + String host, int port, int cto, int to, + Properties props, String prefix, + SocketFactory sf, boolean useSSL) + throws IOException { + Socket socket = null; + + if (logger.isLoggable(Level.FINEST)) + logger.finest("create socket: prefix " + prefix + + ", localaddr " + localaddr + ", localport " + localport + + ", host " + host + ", port " + port + + ", connection timeout " + cto + ", timeout " + to + + ", socket factory " + sf + ", useSSL " + useSSL); + + String proxyHost = props.getProperty(prefix + ".proxy.host", null); + String proxyUser = props.getProperty(prefix + ".proxy.user", null); + String proxyPassword = props.getProperty(prefix + ".proxy.password", null); + int proxyPort = 80; + String socksHost = null; + int socksPort = 1080; + String err = null; + + if (proxyHost != null) { + int i = proxyHost.indexOf(':'); + if (i >= 0) { + try { + proxyPort = Integer.parseInt(proxyHost.substring(i + 1)); + } catch (NumberFormatException ex) { + // ignore it + } + proxyHost = proxyHost.substring(0, i); + } + proxyPort = PropUtil.getIntProperty(props, + prefix + ".proxy.port", proxyPort); + err = "Using web proxy host, port: " + proxyHost + ", " + proxyPort; + if (logger.isLoggable(Level.FINER)) { + logger.finer("web proxy host " + proxyHost + ", port " + proxyPort); + if (proxyUser != null) + logger.finer("web proxy user " + proxyUser + ", password " + + (proxyPassword == null ? "" : "")); + } + } else if ((socksHost = + props.getProperty(prefix + ".socks.host", null)) != null) { + int i = socksHost.indexOf(':'); + if (i >= 0) { + try { + socksPort = Integer.parseInt(socksHost.substring(i + 1)); + } catch (NumberFormatException ex) { + // ignore it + } + socksHost = socksHost.substring(0, i); + } + socksPort = PropUtil.getIntProperty(props, + prefix + ".socks.port", socksPort); + err = "Using SOCKS host, port: " + socksHost + ", " + socksPort; + if (logger.isLoggable(Level.FINER)) + logger.finer("socks host " + socksHost + ", port " + socksPort); + } + + if (sf != null && !(sf instanceof SSLSocketFactory)) + socket = sf.createSocket(); + if (socket == null) { + if (socksHost != null) { + socket = new Socket( + new java.net.Proxy(java.net.Proxy.Type.SOCKS, + new InetSocketAddress(socksHost, socksPort))); + } else if (PropUtil.getBooleanProperty(props, + prefix + ".usesocketchannels", false)) { + logger.finer("using SocketChannels"); + socket = SocketChannel.open().socket(); + } else + socket = new Socket(); + } + if (to >= 0) { + if (logger.isLoggable(Level.FINEST)) + logger.finest("set socket read timeout " + to); + socket.setSoTimeout(to); + } + int writeTimeout = PropUtil.getIntProperty(props, + prefix + ".writetimeout", -1); + if (writeTimeout != -1) { // wrap original + if (logger.isLoggable(Level.FINEST)) + logger.finest("set socket write timeout " + writeTimeout); + socket = new WriteTimeoutSocket(socket, writeTimeout); + } + if (localaddr != null) + socket.bind(new InetSocketAddress(localaddr, localport)); + try { + logger.finest("connecting..."); + if (proxyHost != null) + proxyConnect(socket, proxyHost, proxyPort, + proxyUser, proxyPassword, host, port, cto); + else if (cto >= 0) + socket.connect(new InetSocketAddress(host, port), cto); + else + socket.connect(new InetSocketAddress(host, port)); + logger.finest("success!"); + } catch (IOException ex) { + logger.log(Level.FINEST, "connection failed", ex); + throw new SocketConnectException(err, ex, host, port, cto); + } + + /* + * If we want an SSL connection and we didn't get an SSLSocket, + * wrap our plain Socket with an SSLSocket. + */ + if ((useSSL || sf instanceof SSLSocketFactory) && + !(socket instanceof SSLSocket)) { + String trusted; + SSLSocketFactory ssf; + if ((trusted = props.getProperty(prefix + ".ssl.trust")) != null) { + try { + MailSSLSocketFactory msf = new MailSSLSocketFactory(); + if (trusted.equals("*")) + msf.setTrustAllHosts(true); + else + msf.setTrustedHosts(trusted.split("\\s+")); + ssf = msf; + } catch (GeneralSecurityException gex) { + IOException ioex = new IOException( + "Can't create MailSSLSocketFactory"); + ioex.initCause(gex); + throw ioex; + } + } else if (sf instanceof SSLSocketFactory) + ssf = (SSLSocketFactory)sf; + else + ssf = (SSLSocketFactory)SSLSocketFactory.getDefault(); + socket = ssf.createSocket(socket, host, port, true); + sf = ssf; + } + + /* + * No matter how we created the socket, if it turns out to be an + * SSLSocket, configure it. + */ + configureSSLSocket(socket, host, props, prefix, sf); + + return socket; + } + + /** + * Return a socket factory of the specified class. + */ + private static SocketFactory getSocketFactory(String sfClass) + throws ClassNotFoundException, + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + if (sfClass == null || sfClass.length() == 0) + return null; + + // dynamically load the class + + ClassLoader cl = getContextClassLoader(); + Class clsSockFact = null; + if (cl != null) { + try { + clsSockFact = Class.forName(sfClass, false, cl); + } catch (ClassNotFoundException cex) { } + } + if (clsSockFact == null) + clsSockFact = Class.forName(sfClass); + // get & invoke the getDefault() method + Method mthGetDefault = clsSockFact.getMethod("getDefault", + new Class[]{}); + SocketFactory sf = (SocketFactory) + mthGetDefault.invoke(new Object(), new Object[]{}); + return sf; + } + + /** + * Start TLS on an existing socket. + * Supports the "STARTTLS" command in many protocols. + * This version for compatibility with possible third party code + * that might've used this API even though it shouldn't. + * + * @param socket the existing socket + * @return the wrapped Socket + * @exception IOException for I/O errors + * @deprecated + */ + @Deprecated + public static Socket startTLS(Socket socket) throws IOException { + return startTLS(socket, new Properties(), "socket"); + } + + /** + * Start TLS on an existing socket. + * Supports the "STARTTLS" command in many protocols. + * This version for compatibility with possible third party code + * that might've used this API even though it shouldn't. + * + * @param socket the existing socket + * @param props the properties + * @param prefix the property prefix + * @return the wrapped Socket + * @exception IOException for I/O errors + * @deprecated + */ + @Deprecated + public static Socket startTLS(Socket socket, Properties props, + String prefix) throws IOException { + InetAddress a = socket.getInetAddress(); + String host = a.getHostName(); + return startTLS(socket, host, props, prefix); + } + + /** + * Start TLS on an existing socket. + * Supports the "STARTTLS" command in many protocols. + * + * @param socket the existing socket + * @param host the host the socket is connected to + * @param props the properties + * @param prefix the property prefix + * @return the wrapped Socket + * @exception IOException for I/O errors + */ + public static Socket startTLS(Socket socket, String host, Properties props, + String prefix) throws IOException { + int port = socket.getPort(); + if (logger.isLoggable(Level.FINER)) + logger.finer("startTLS host " + host + ", port " + port); + + String sfErr = "unknown socket factory"; + try { + SSLSocketFactory ssf = null; + SocketFactory sf = null; + + // first, look for an SSL socket factory + Object sfo = props.get(prefix + ".ssl.socketFactory"); + if (sfo instanceof SocketFactory) { + sf = (SocketFactory)sfo; + sfErr = "SSL socket factory instance " + sf; + } + if (sf == null) { + String sfClass = + props.getProperty(prefix + ".ssl.socketFactory.class"); + sf = getSocketFactory(sfClass); + sfErr = "SSL socket factory class " + sfClass; + } + if (sf != null && sf instanceof SSLSocketFactory) + ssf = (SSLSocketFactory)sf; + + // next, look for a regular socket factory that happens to be + // an SSL socket factory + if (ssf == null) { + sfo = props.get(prefix + ".socketFactory"); + if (sfo instanceof SocketFactory) { + sf = (SocketFactory)sfo; + sfErr = "socket factory instance " + sf; + } + if (sf == null) { + String sfClass = + props.getProperty(prefix + ".socketFactory.class"); + sf = getSocketFactory(sfClass); + sfErr = "socket factory class " + sfClass; + } + if (sf != null && sf instanceof SSLSocketFactory) + ssf = (SSLSocketFactory)sf; + } + + // finally, use the default SSL socket factory + if (ssf == null) { + String trusted; + if ((trusted = props.getProperty(prefix + ".ssl.trust")) != + null) { + try { + MailSSLSocketFactory msf = new MailSSLSocketFactory(); + if (trusted.equals("*")) + msf.setTrustAllHosts(true); + else + msf.setTrustedHosts(trusted.split("\\s+")); + ssf = msf; + sfErr = "mail SSL socket factory"; + } catch (GeneralSecurityException gex) { + IOException ioex = new IOException( + "Can't create MailSSLSocketFactory"); + ioex.initCause(gex); + throw ioex; + } + } else { + ssf = (SSLSocketFactory)SSLSocketFactory.getDefault(); + sfErr = "default SSL socket factory"; + } + } + + socket = ssf.createSocket(socket, host, port, true); + configureSSLSocket(socket, host, props, prefix, ssf); + } catch (Exception ex) { + if (ex instanceof InvocationTargetException) { + Throwable t = + ((InvocationTargetException)ex).getTargetException(); + if (t instanceof Exception) + ex = (Exception)t; + } + if (ex instanceof IOException) + throw (IOException)ex; + // wrap anything else before sending it on + IOException ioex = new IOException( + "Exception in startTLS using " + sfErr + + ": host, port: " + + host + ", " + port + + "; Exception: " + ex); + ioex.initCause(ex); + throw ioex; + } + return socket; + } + + /** + * Configure the SSL options for the socket (if it's an SSL socket), + * based on the mail..ssl.protocols and + * mail..ssl.ciphersuites properties. + * Check the identity of the server as specified by the + * mail..ssl.checkserveridentity property. + */ + private static void configureSSLSocket(Socket socket, String host, + Properties props, String prefix, SocketFactory sf) + throws IOException { + if (!(socket instanceof SSLSocket)) + return; + SSLSocket sslsocket = (SSLSocket)socket; + + String protocols = props.getProperty(prefix + ".ssl.protocols", null); + if (protocols != null) + sslsocket.setEnabledProtocols(stringArray(protocols)); + else { + /* + * The UW IMAP server insists on at least the TLSv1 + * protocol for STARTTLS, and won't accept the old SSLv2 + * or SSLv3 protocols. Here we enable only the non-SSL + * protocols. XXX - this should probably be parameterized. + */ + String[] prots = sslsocket.getEnabledProtocols(); + if (logger.isLoggable(Level.FINER)) + logger.finer("SSL enabled protocols before " + + Arrays.asList(prots)); + List eprots = new ArrayList<>(); + for (int i = 0; i < prots.length; i++) { + if (prots[i] != null && !prots[i].startsWith("SSL")) + eprots.add(prots[i]); + } + sslsocket.setEnabledProtocols( + eprots.toArray(new String[eprots.size()])); + } + String ciphers = props.getProperty(prefix + ".ssl.ciphersuites", null); + if (ciphers != null) + sslsocket.setEnabledCipherSuites(stringArray(ciphers)); + if (logger.isLoggable(Level.FINER)) { + logger.finer("SSL enabled protocols after " + + Arrays.asList(sslsocket.getEnabledProtocols())); + logger.finer("SSL enabled ciphers after " + + Arrays.asList(sslsocket.getEnabledCipherSuites())); + } + + /* + * Force the handshake to be done now so that we can report any + * errors (e.g., certificate errors) to the caller of the startTLS + * method. + */ + sslsocket.startHandshake(); + + /* + * Check server identity and trust. + */ + boolean idCheck = PropUtil.getBooleanProperty(props, + prefix + ".ssl.checkserveridentity", false); + if (idCheck) + checkServerIdentity(host, sslsocket); + if (sf instanceof MailSSLSocketFactory) { + MailSSLSocketFactory msf = (MailSSLSocketFactory)sf; + if (!msf.isServerTrusted(host, sslsocket)) { + throw cleanupAndThrow(sslsocket, + new IOException("Server is not trusted: " + host)); + } + } + } + + private static IOException cleanupAndThrow(Socket socket, IOException ife) { + try { + socket.close(); + } catch (Throwable thr) { + if (isRecoverable(thr)) { + ife.addSuppressed(thr); + } else { + thr.addSuppressed(ife); + if (thr instanceof Error) { + throw (Error) thr; + } + if (thr instanceof RuntimeException) { + throw (RuntimeException) thr; + } + throw new RuntimeException("unexpected exception", thr); + } + } + return ife; + } + + private static boolean isRecoverable(Throwable t) { + return (t instanceof Exception) || (t instanceof LinkageError); + } + + /** + * Check the server from the Socket connection against the server name(s) + * as expressed in the server certificate (RFC 2595 check). + * + * @param server name of the server expected + * @param sslSocket SSLSocket connected to the server + * @exception IOException if we can't verify identity of server + */ + private static void checkServerIdentity(String server, SSLSocket sslSocket) + throws IOException { + + // Check against the server name(s) as expressed in server certificate + try { + java.security.cert.Certificate[] certChain = + sslSocket.getSession().getPeerCertificates(); + if (certChain != null && certChain.length > 0 && + certChain[0] instanceof X509Certificate && + matchCert(server, (X509Certificate)certChain[0])) + return; + } catch (SSLPeerUnverifiedException e) { + sslSocket.close(); + IOException ioex = new IOException( + "Can't verify identity of server: " + server); + ioex.initCause(e); + throw ioex; + } + + // If we get here, there is nothing to consider the server as trusted. + sslSocket.close(); + throw new IOException("Can't verify identity of server: " + server); + } + + /** + * Do any of the names in the cert match the server name? + * + * @param server name of the server expected + * @param cert X509Certificate to get the subject's name from + * @return true if it matches + */ + private static boolean matchCert(String server, X509Certificate cert) { + if (logger.isLoggable(Level.FINER)) + logger.finer("matchCert server " + + server + ", cert " + cert); + + /* + * First, try to use sun.security.util.HostnameChecker, + * which exists in Sun's JDK starting with 1.4.1. + * We use reflection to access it in case it's not available + * in the JDK we're running on. + */ + try { + Class hnc = Class.forName("sun.security.util.HostnameChecker"); + // invoke HostnameChecker.getInstance(HostnameChecker.TYPE_LDAP) + // HostnameChecker.TYPE_LDAP == 2 + // LDAP requires the same regex handling as we need + Method getInstance = hnc.getMethod("getInstance", + new Class[] { byte.class }); + Object hostnameChecker = getInstance.invoke(new Object(), + new Object[] { Byte.valueOf((byte)2) }); + + // invoke hostnameChecker.match( server, cert) + if (logger.isLoggable(Level.FINER)) + logger.finer("using sun.security.util.HostnameChecker"); + Method match = hnc.getMethod("match", + new Class[] { String.class, X509Certificate.class }); + try { + match.invoke(hostnameChecker, new Object[] { server, cert }); + return true; + } catch (InvocationTargetException cex) { + logger.log(Level.FINER, "HostnameChecker FAIL", cex); + return false; + } + } catch (Exception ex) { + logger.log(Level.FINER, "NO sun.security.util.HostnameChecker", ex); + // ignore failure and continue below + } + + /* + * Lacking HostnameChecker, we implement a crude version of + * the same checks ourselves. + */ + try { + /* + * Check each of the subjectAltNames. + * XXX - only checks DNS names, should also handle + * case where server name is a literal IP address + */ + Collection> names = cert.getSubjectAlternativeNames(); + if (names != null) { + boolean foundName = false; + for (Iterator> it = names.iterator(); it.hasNext(); ) { + List nameEnt = it.next(); + Integer type = (Integer)nameEnt.get(0); + if (type.intValue() == 2) { // 2 == dNSName + foundName = true; + String name = (String)nameEnt.get(1); + if (logger.isLoggable(Level.FINER)) + logger.finer("found name: " + name); + if (matchServer(server, name)) + return true; + } + } + if (foundName) // found a name, but no match + return false; + } + } catch (CertificateParsingException ex) { + // ignore it + } + + // XXX - following is a *very* crude parse of the name and ignores + // all sorts of important issues such as quoting + Pattern p = Pattern.compile("CN=([^,]*)"); + Matcher m = p.matcher(cert.getSubjectX500Principal().getName()); + if (m.find() && matchServer(server, m.group(1).trim())) + return true; + + return false; + } + + /** + * Does the server we're expecting to connect to match the + * given name from the server's certificate? + * + * @param server name of the server expected + * @param name name from the server's certificate + */ + private static boolean matchServer(String server, String name) { + if (logger.isLoggable(Level.FINER)) + logger.finer("match server " + server + " with " + name); + if (name.startsWith("*.")) { + // match "foo.example.com" with "*.example.com" + String tail = name.substring(2); + if (tail.length() == 0) + return false; + int off = server.length() - tail.length(); + if (off < 1) + return false; + // if tail matches and is preceeded by "." + return server.charAt(off - 1) == '.' && + server.regionMatches(true, off, tail, 0, tail.length()); + } else + return server.equalsIgnoreCase(name); + } + + /** + * Use the HTTP CONNECT protocol to connect to a + * site through an HTTP proxy server.

+ * + * Protocol is roughly: + *

+     * CONNECT : HTTP/1.1
+     * Host: :
+     * 
+     *
+     * HTTP/1.1 200 Connect established
+     * 
+     * 
+     * 
+ */ + private static void proxyConnect(Socket socket, + String proxyHost, int proxyPort, + String proxyUser, String proxyPassword, + String host, int port, int cto) + throws IOException { + if (logger.isLoggable(Level.FINE)) + logger.fine("connecting through proxy " + + proxyHost + ":" + proxyPort + " to " + + host + ":" + port); + if (cto >= 0) + socket.connect(new InetSocketAddress(proxyHost, proxyPort), cto); + else + socket.connect(new InetSocketAddress(proxyHost, proxyPort)); + PrintStream os = new PrintStream(socket.getOutputStream(), false, + StandardCharsets.UTF_8.name()); + StringBuilder requestBuilder = new StringBuilder(); + requestBuilder.append("CONNECT ").append(host).append(":").append(port). + append(" HTTP/1.1\r\n"); + requestBuilder.append("Host: ").append(host).append(":").append(port). + append("\r\n"); + if (proxyUser != null && proxyPassword != null) { + byte[] upbytes = (proxyUser + ':' + proxyPassword). + getBytes(StandardCharsets.UTF_8); + String proxyHeaderValue = new String( + BASE64EncoderStream.encode(upbytes), + StandardCharsets.US_ASCII); + requestBuilder.append("Proxy-Authorization: Basic "). + append(proxyHeaderValue).append("\r\n"); + } + requestBuilder.append("Proxy-Connection: keep-alive\r\n\r\n"); + os.print(requestBuilder.toString()); + os.flush(); + BufferedReader r = new BufferedReader(new InputStreamReader( + socket.getInputStream(), StandardCharsets.UTF_8)); + String line; + boolean first = true; + while ((line = r.readLine()) != null) { + if (line.length() == 0) + break; + logger.finest(line); + if (first) { + StringTokenizer st = new StringTokenizer(line); + String http = st.nextToken(); + String code = st.nextToken(); + if (!code.equals("200")) { + try { + socket.close(); + } catch (IOException ioex) { + // ignored + } + ConnectException ex = new ConnectException( + "connection through proxy " + + proxyHost + ":" + proxyPort + " to " + + host + ":" + port + " failed: " + line); + logger.log(Level.FINE, "connect failed", ex); + throw ex; + } + first = false; + } + } + } + + /** + * Parse a string into whitespace separated tokens + * and return the tokens in an array. + */ + private static String[] stringArray(String s) { + StringTokenizer st = new StringTokenizer(s); + List tokens = new ArrayList<>(); + while (st.hasMoreTokens()) + tokens.add(st.nextToken()); + return tokens.toArray(new String[tokens.size()]); + } + + /** + * Convenience method to get our context class loader. + * Assert any privileges we might have and then call the + * Thread.getContextClassLoader method. + */ + private static ClassLoader getContextClassLoader() { + return + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public ClassLoader run() { + ClassLoader cl = null; + try { + cl = Thread.currentThread().getContextClassLoader(); + } catch (SecurityException ex) { } + return cl; + } + }); + } +} diff --git a/app/src/main/java/com/sun/mail/util/TraceInputStream.java b/app/src/main/java/com/sun/mail/util/TraceInputStream.java new file mode 100644 index 0000000000..fceb7ac45a --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/TraceInputStream.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; +import java.util.logging.Level; + +/** + * This class is a FilterInputStream that writes the bytes + * being read from the given input stream into the given output + * stream. This class is typically used to provide a trace of + * the data that is being retrieved from an input stream. + * + * @author John Mani + */ + +public class TraceInputStream extends FilterInputStream { + private boolean trace = false; + private boolean quote = false; + private OutputStream traceOut; + + /** + * Creates an input stream filter built on top of the specified + * input stream. + * + * @param in the underlying input stream. + * @param logger log trace here + */ + public TraceInputStream(InputStream in, MailLogger logger) { + super(in); + this.trace = logger.isLoggable(Level.FINEST); + this.traceOut = new LogOutputStream(logger); + } + + /** + * Creates an input stream filter built on top of the specified + * input stream. + * + * @param in the underlying input stream. + * @param traceOut the trace stream. + */ + public TraceInputStream(InputStream in, OutputStream traceOut) { + super(in); + this.traceOut = traceOut; + } + + /** + * Set trace mode. + * @param trace the trace mode + */ + public void setTrace(boolean trace) { + this.trace = trace; + } + + /** + * Set quote mode. + * @param quote the quote mode + */ + public void setQuote(boolean quote) { + this.quote = quote; + } + + /** + * Reads the next byte of data from this input stream. Returns + * -1 if no data is available. Writes out the read + * byte into the trace stream, if trace mode is true + */ + @Override + public int read() throws IOException { + int b = in.read(); + if (trace && b != -1) { + if (quote) + writeByte(b); + else + traceOut.write(b); + } + return b; + } + + /** + * Reads up to len bytes of data from this input stream + * into an array of bytes. Returns -1 if no more data + * is available. Writes out the read bytes into the trace stream, if + * trace mode is true + */ + @Override + public int read(byte b[], int off, int len) throws IOException { + int count = in.read(b, off, len); + if (trace && count != -1) { + if (quote) { + for (int i = 0; i < count; i++) + writeByte(b[off + i]); + } else + traceOut.write(b, off, count); + } + return count; + } + + /** + * Write a byte in a way that every byte value is printable ASCII. + */ + private final void writeByte(int b) throws IOException { + b &= 0xff; + if (b > 0x7f) { + traceOut.write('M'); + traceOut.write('-'); + b &= 0x7f; + } + if (b == '\r') { + traceOut.write('\\'); + traceOut.write('r'); + } else if (b == '\n') { + traceOut.write('\\'); + traceOut.write('n'); + traceOut.write('\n'); + } else if (b == '\t') { + traceOut.write('\\'); + traceOut.write('t'); + } else if (b < ' ') { + traceOut.write('^'); + traceOut.write('@' + b); + } else { + traceOut.write(b); + } + } +} diff --git a/app/src/main/java/com/sun/mail/util/TraceOutputStream.java b/app/src/main/java/com/sun/mail/util/TraceOutputStream.java new file mode 100644 index 0000000000..28f282015a --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/TraceOutputStream.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; +import java.util.logging.Level; + +/** + * This class is a subclass of DataOutputStream that copies the + * data being written into the DataOutputStream into another output + * stream. This class is used here to provide a debug trace of the + * stuff thats being written out into the DataOutputStream. + * + * @author John Mani + */ + +public class TraceOutputStream extends FilterOutputStream { + private boolean trace = false; + private boolean quote = false; + private OutputStream traceOut; + + /** + * Creates an output stream filter built on top of the specified + * underlying output stream. + * + * @param out the underlying output stream. + * @param logger log trace here + */ + public TraceOutputStream(OutputStream out, MailLogger logger) { + super(out); + this.trace = logger.isLoggable(Level.FINEST); + this.traceOut = new LogOutputStream(logger);; + } + + /** + * Creates an output stream filter built on top of the specified + * underlying output stream. + * + * @param out the underlying output stream. + * @param traceOut the trace stream. + */ + public TraceOutputStream(OutputStream out, OutputStream traceOut) { + super(out); + this.traceOut = traceOut; + } + + /** + * Set the trace mode. + * + * @param trace the trace mode + */ + public void setTrace(boolean trace) { + this.trace = trace; + } + + /** + * Set quote mode. + * @param quote the quote mode + */ + public void setQuote(boolean quote) { + this.quote = quote; + } + + /** + * Writes the specified byte to this output stream. + * Writes out the byte into the trace stream if the trace mode + * is true + * + * @param b the byte to write + * @exception IOException for I/O errors + */ + @Override + public void write(int b) throws IOException { + if (trace) { + if (quote) + writeByte(b); + else + traceOut.write(b); + } + out.write(b); + } + + /** + * Writes b.length bytes to this output stream. + * Writes out the bytes into the trace stream if the trace + * mode is true + * + * @param b bytes to write + * @param off offset in array + * @param len number of bytes to write + * @exception IOException for I/O errors + */ + @Override + public void write(byte b[], int off, int len) throws IOException { + if (trace) { + if (quote) { + for (int i = 0; i < len; i++) + writeByte(b[off + i]); + } else + traceOut.write(b, off, len); + } + out.write(b, off, len); + } + + /** + * Write a byte in a way that every byte value is printable ASCII. + */ + private final void writeByte(int b) throws IOException { + b &= 0xff; + if (b > 0x7f) { + traceOut.write('M'); + traceOut.write('-'); + b &= 0x7f; + } + if (b == '\r') { + traceOut.write('\\'); + traceOut.write('r'); + } else if (b == '\n') { + traceOut.write('\\'); + traceOut.write('n'); + traceOut.write('\n'); + } else if (b == '\t') { + traceOut.write('\\'); + traceOut.write('t'); + } else if (b < ' ') { + traceOut.write('^'); + traceOut.write('@' + b); + } else { + traceOut.write(b); + } + } +} diff --git a/app/src/main/java/com/sun/mail/util/UUDecoderStream.java b/app/src/main/java/com/sun/mail/util/UUDecoderStream.java new file mode 100644 index 0000000000..82f957cc0b --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/UUDecoderStream.java @@ -0,0 +1,334 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a UUDecoder. It is implemented as + * a FilterInputStream, so one can just wrap this class around + * any input stream and read bytes from this filter. The decoding + * is done as the bytes are read out. + * + * @author John Mani + * @author Bill Shannon + */ + +public class UUDecoderStream extends FilterInputStream { + private String name; + private int mode; + + private byte[] buffer = new byte[45]; // max decoded chars in a line = 45 + private int bufsize = 0; // size of the cache + private int index = 0; // index into the cache + private boolean gotPrefix = false; + private boolean gotEnd = false; + private LineInputStream lin; + private boolean ignoreErrors; + private boolean ignoreMissingBeginEnd; + private String readAhead; + + /** + * Create a UUdecoder that decodes the specified input stream. + * The System property mail.mime.uudecode.ignoreerrors + * controls whether errors in the encoded data cause an exception + * or are ignored. The default is false (errors cause exception). + * The System property mail.mime.uudecode.ignoremissingbeginend + * controls whether a missing begin or end line cause an exception + * or are ignored. The default is false (errors cause exception). + * @param in the input stream + */ + public UUDecoderStream(InputStream in) { + super(in); + lin = new LineInputStream(in); + // default to false + ignoreErrors = PropUtil.getBooleanSystemProperty( + "mail.mime.uudecode.ignoreerrors", false); + // default to false + ignoreMissingBeginEnd = PropUtil.getBooleanSystemProperty( + "mail.mime.uudecode.ignoremissingbeginend", false); + } + + /** + * Create a UUdecoder that decodes the specified input stream. + * @param in the input stream + * @param ignoreErrors ignore errors? + * @param ignoreMissingBeginEnd ignore missing begin or end? + */ + public UUDecoderStream(InputStream in, boolean ignoreErrors, + boolean ignoreMissingBeginEnd) { + super(in); + lin = new LineInputStream(in); + this.ignoreErrors = ignoreErrors; + this.ignoreMissingBeginEnd = ignoreMissingBeginEnd; + } + + /** + * Read the next decoded byte from this input stream. The byte + * is returned as an int in the range 0 + * to 255. If no byte is available because the end of + * the stream has been reached, the value -1 is returned. + * This method blocks until input data is available, the end of the + * stream is detected, or an exception is thrown. + * + * @return next byte of data, or -1 if the end of + * stream is reached. + * @exception IOException if an I/O error occurs. + * @see java.io.FilterInputStream#in + */ + @Override + public int read() throws IOException { + if (index >= bufsize) { + readPrefix(); + if (!decode()) + return -1; + index = 0; // reset index into buffer + } + return buffer[index++] & 0xff; // return lower byte + } + + @Override + public int read(byte[] buf, int off, int len) throws IOException { + int i, c; + for (i = 0; i < len; i++) { + if ((c = read()) == -1) { + if (i == 0) // At end of stream, so we should + i = -1; // return -1, NOT 0. + break; + } + buf[off+i] = (byte)c; + } + return i; + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public int available() throws IOException { + // This is only an estimate, since in.available() + // might include CRLFs too .. + return ((in.available() * 3)/4 + (bufsize-index)); + } + + /** + * Get the "name" field from the prefix. This is meant to + * be the pathname of the decoded file + * + * @return name of decoded file + * @exception IOException if an I/O error occurs. + */ + public String getName() throws IOException { + readPrefix(); + return name; + } + + /** + * Get the "mode" field from the prefix. This is the permission + * mode of the source file. + * + * @return permission mode of source file + * @exception IOException if an I/O error occurs. + */ + public int getMode() throws IOException { + readPrefix(); + return mode; + } + + /** + * UUencoded streams start off with the line: + * "begin " + * Search for this prefix and gobble it up. + */ + private void readPrefix() throws IOException { + if (gotPrefix) // got the prefix + return; + + mode = 0666; // defaults, overridden below + name = "encoder.buf"; // same default used by encoder + String line; + for (;;) { + // read till we get the prefix: "begin MODE FILENAME" + line = lin.readLine(); // NOTE: readLine consumes CRLF pairs too + if (line == null) { + if (!ignoreMissingBeginEnd) + throw new DecodingException("UUDecoder: Missing begin"); + // at EOF, fake it + gotPrefix = true; + gotEnd = true; + break; + } + if (line.regionMatches(false, 0, "begin", 0, 5)) { + try { + mode = Integer.parseInt(line.substring(6,9)); + } catch (NumberFormatException ex) { + if (!ignoreErrors) + throw new DecodingException( + "UUDecoder: Error in mode: " + ex.toString()); + } + if (line.length() > 10) { + name = line.substring(10); + } else { + if (!ignoreErrors) + throw new DecodingException( + "UUDecoder: Missing name: " + line); + } + gotPrefix = true; + break; + } else if (ignoreMissingBeginEnd && line.length() != 0) { + int count = line.charAt(0); + count = (count - ' ') & 0x3f; + int need = ((count * 8)+5)/6; + if (need == 0 || line.length() >= need + 1) { + /* + * Looks like a legitimate encoded line. + * Pretend we saw the "begin" line and + * save this line for later processing in + * decode(). + */ + readAhead = line; + gotPrefix = true; // fake it + break; + } + } + } + } + + private boolean decode() throws IOException { + + if (gotEnd) + return false; + bufsize = 0; + int count = 0; + String line; + for (;;) { + /* + * If we ignored a missing "begin", the first line + * will be saved in readAhead. + */ + if (readAhead != null) { + line = readAhead; + readAhead = null; + } else + line = lin.readLine(); + + /* + * Improperly encoded data sometimes omits the zero length + * line that starts with a space character, we detect the + * following "end" line here. + */ + if (line == null) { + if (!ignoreMissingBeginEnd) + throw new DecodingException( + "UUDecoder: Missing end at EOF"); + gotEnd = true; + return false; + } + if (line.equals("end")) { + gotEnd = true; + return false; + } + if (line.length() == 0) + continue; + count = line.charAt(0); + if (count < ' ') { + if (!ignoreErrors) + throw new DecodingException( + "UUDecoder: Buffer format error"); + continue; + } + + /* + * The first character in a line is the number of original (not + * the encoded atoms) characters in the line. Note that all the + * code below has to handle the character that indicates + * end of encoded stream. + */ + count = (count - ' ') & 0x3f; + + if (count == 0) { + line = lin.readLine(); + if (line == null || !line.equals("end")) { + if (!ignoreMissingBeginEnd) + throw new DecodingException( + "UUDecoder: Missing End after count 0 line"); + } + gotEnd = true; + return false; + } + + int need = ((count * 8)+5)/6; +//System.out.println("count " + count + ", need " + need + ", len " + line.length()); + if (line.length() < need + 1) { + if (!ignoreErrors) + throw new DecodingException( + "UUDecoder: Short buffer error"); + continue; + } + + // got a line we're committed to, break out and decode it + break; + } + + int i = 1; + byte a, b; + /* + * A correct uuencoder always encodes 3 characters at a time, even + * if there aren't 3 characters left. But since some people out + * there have broken uuencoders we handle the case where they + * don't include these "unnecessary" characters. + */ + while (bufsize < count) { + // continue decoding until we get 'count' decoded chars + a = (byte)((line.charAt(i++) - ' ') & 0x3f); + b = (byte)((line.charAt(i++) - ' ') & 0x3f); + buffer[bufsize++] = (byte)(((a << 2) & 0xfc) | ((b >>> 4) & 3)); + + if (bufsize < count) { + a = b; + b = (byte)((line.charAt(i++) - ' ') & 0x3f); + buffer[bufsize++] = + (byte)(((a << 4) & 0xf0) | ((b >>> 2) & 0xf)); + } + + if (bufsize < count) { + a = b; + b = (byte)((line.charAt(i++) - ' ') & 0x3f); + buffer[bufsize++] = (byte)(((a << 6) & 0xc0) | (b & 0x3f)); + } + } + return true; + } + + /*** begin TEST program ***** + public static void main(String argv[]) throws Exception { + FileInputStream infile = new FileInputStream(argv[0]); + UUDecoderStream decoder = new UUDecoderStream(infile); + int c; + + try { + while ((c = decoder.read()) != -1) + System.out.write(c); + System.out.flush(); + } catch (Exception e) { + e.printStackTrace(); + } + } + **** end TEST program ****/ +} diff --git a/app/src/main/java/com/sun/mail/util/UUEncoderStream.java b/app/src/main/java/com/sun/mail/util/UUEncoderStream.java new file mode 100644 index 0000000000..183cc34737 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/UUEncoderStream.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a UUEncoder. It is implemented as + * a FilterOutputStream, so one can just wrap this class around + * any output stream and write bytes into this filter. The Encoding + * is done as the bytes are written out. + * + * @author John Mani + */ + +public class UUEncoderStream extends FilterOutputStream { + private byte[] buffer; // cache of bytes that are yet to be encoded + private int bufsize = 0; // size of the cache + private boolean wrotePrefix = false; + private boolean wroteSuffix = false; + + private String name; // name of file + private int mode; // permissions mode + + /** + * Create a UUencoder that encodes the specified input stream + * @param out the output stream + */ + public UUEncoderStream(OutputStream out) { + this(out, "encoder.buf", 0644); + } + + /** + * Create a UUencoder that encodes the specified input stream + * @param out the output stream + * @param name Specifies a name for the encoded buffer + */ + public UUEncoderStream(OutputStream out, String name) { + this(out, name, 0644); + } + + /** + * Create a UUencoder that encodes the specified input stream + * @param out the output stream + * @param name Specifies a name for the encoded buffer + * @param mode Specifies permission mode for the encoded buffer + */ + public UUEncoderStream(OutputStream out, String name, int mode) { + super(out); + this.name = name; + this.mode = mode; + buffer = new byte[45]; + } + + /** + * Set up the buffer name and permission mode. + * This method has any effect only if it is invoked before + * you start writing into the output stream + * + * @param name the buffer name + * @param mode the permission mode + */ + public void setNameMode(String name, int mode) { + this.name = name; + this.mode = mode; + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + for (int i = 0; i < len; i++) + write(b[off + i]); + } + + @Override + public void write(byte[] data) throws IOException { + write(data, 0, data.length); + } + + @Override + public void write(int c) throws IOException { + /* buffer up characters till we get a line's worth, then encode + * and write them out. Max number of characters allowed per + * line is 45. + */ + buffer[bufsize++] = (byte)c; + if (bufsize == 45) { + writePrefix(); + encode(); + bufsize = 0; + } + } + + @Override + public void flush() throws IOException { + if (bufsize > 0) { // If there's unencoded characters in the buffer + writePrefix(); + encode(); // .. encode them + bufsize = 0; + } + writeSuffix(); + out.flush(); + } + + @Override + public void close() throws IOException { + flush(); + out.close(); + } + + /** + * Write out the prefix: "begin " + */ + private void writePrefix() throws IOException { + if (!wrotePrefix) { + // name should be ASCII, but who knows... + PrintStream ps = new PrintStream(out, false, "utf-8"); + ps.format("begin %o %s%n", mode, name); + ps.flush(); + wrotePrefix = true; + } + } + + /** + * Write a single line containing space and the suffix line + * containing the single word "end" (terminated by a newline) + */ + private void writeSuffix() throws IOException { + if (!wroteSuffix) { + PrintStream ps = new PrintStream(out, false, "us-ascii"); + ps.println(" \nend"); + ps.flush(); + wroteSuffix = true; + } + } + + /** + * Encode a line. + * Start off with the character count, followed by the encoded atoms + * and terminate with LF. (or is it CRLF or the local line-terminator ?) + * Take three bytes and encodes them into 4 characters + * If bufsize if not a multiple of 3, the remaining bytes are filled + * with '1'. This insures that the last line won't end in spaces + * and potentiallly be truncated. + */ + private void encode() throws IOException { + byte a, b, c; + int c1, c2, c3, c4; + int i = 0; + + // Start off with the count of characters in the line + out.write((bufsize & 0x3f) + ' '); + + while (i < bufsize) { + a = buffer[i++]; + if (i < bufsize) { + b = buffer[i++]; + if (i < bufsize) + c = buffer[i++]; + else // default c to 1 + c = 1; + } + else { // default b & c to 1 + b = 1; + c = 1; + } + + c1 = (a >>> 2) & 0x3f; + c2 = ((a << 4) & 0x30) | ((b >>> 4) & 0xf); + c3 = ((b << 2) & 0x3c) | ((c >>> 6) & 0x3); + c4 = c & 0x3f; + out.write(c1 + ' '); + out.write(c2 + ' '); + out.write(c3 + ' '); + out.write(c4 + ' '); + } + // Terminate with LF. (should it be CRLF or local line-terminator ?) + out.write('\n'); + } + + /**** begin TEST program ***** + public static void main(String argv[]) throws Exception { + FileInputStream infile = new FileInputStream(argv[0]); + UUEncoderStream encoder = new UUEncoderStream(System.out); + int c; + + while ((c = infile.read()) != -1) + encoder.write(c); + encoder.close(); + } + **** end TEST program *****/ +} diff --git a/app/src/main/java/com/sun/mail/util/WriteTimeoutSocket.java b/app/src/main/java/com/sun/mail/util/WriteTimeoutSocket.java new file mode 100644 index 0000000000..1c84b52985 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/WriteTimeoutSocket.java @@ -0,0 +1,401 @@ +/* + * Copyright (c) 2013, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; +import java.net.*; +import java.util.concurrent.*; +import java.util.Collections; +import java.util.Set; +import java.nio.channels.SocketChannel; +import java.lang.reflect.*; + +/** + * A special Socket that uses a ScheduledExecutorService to + * implement timeouts for writes. The write timeout is specified + * (in milliseconds) when the WriteTimeoutSocket is created. + * + * @author Bill Shannon + */ +public class WriteTimeoutSocket extends Socket { + + // delegate all operations to this socket + private final Socket socket; + // to schedule task to cancel write after timeout + private final ScheduledExecutorService ses; + // the timeout, in milliseconds + private final int timeout; + + public WriteTimeoutSocket(Socket socket, int timeout) throws IOException { + this.socket = socket; + // XXX - could share executor with all instances? + this.ses = Executors.newScheduledThreadPool(1); + this.timeout = timeout; + } + + public WriteTimeoutSocket(int timeout) throws IOException { + this(new Socket(), timeout); + } + + public WriteTimeoutSocket(InetAddress address, int port, int timeout) + throws IOException { + this(timeout); + socket.connect(new InetSocketAddress(address, port)); + } + + public WriteTimeoutSocket(InetAddress address, int port, + InetAddress localAddress, int localPort, int timeout) + throws IOException { + this(timeout); + socket.bind(new InetSocketAddress(localAddress, localPort)); + socket.connect(new InetSocketAddress(address, port)); + } + + public WriteTimeoutSocket(String host, int port, int timeout) + throws IOException { + this(timeout); + socket.connect(new InetSocketAddress(host, port)); + } + + public WriteTimeoutSocket(String host, int port, + InetAddress localAddress, int localPort, int timeout) + throws IOException { + this(timeout); + socket.bind(new InetSocketAddress(localAddress, localPort)); + socket.connect(new InetSocketAddress(host, port)); + } + + // override all Socket methods and delegate to underlying Socket + + @Override + public void connect(SocketAddress remote) throws IOException { + socket.connect(remote, 0); + } + + @Override + public void connect(SocketAddress remote, int timeout) throws IOException { + socket.connect(remote, timeout); + } + + @Override + public void bind(SocketAddress local) throws IOException { + socket.bind(local); + } + + @Override + public SocketAddress getRemoteSocketAddress() { + return socket.getRemoteSocketAddress(); + } + + @Override + public SocketAddress getLocalSocketAddress() { + return socket.getLocalSocketAddress(); + } + + @Override + public void setPerformancePreferences(int connectionTime, int latency, + int bandwidth) { + socket.setPerformancePreferences(connectionTime, latency, bandwidth); + } + + @Override + public SocketChannel getChannel() { + return socket.getChannel(); + } + + @Override + public InetAddress getInetAddress() { + return socket.getInetAddress(); + } + + @Override + public InetAddress getLocalAddress() { + return socket.getLocalAddress(); + } + + @Override + public int getPort() { + return socket.getPort(); + } + + @Override + public int getLocalPort() { + return socket.getLocalPort(); + } + + @Override + public InputStream getInputStream() throws IOException { + return socket.getInputStream(); + } + + @Override + public synchronized OutputStream getOutputStream() throws IOException { + // wrap the returned stream to implement write timeout + return new TimeoutOutputStream(socket.getOutputStream(), ses, timeout); + } + + @Override + public void setTcpNoDelay(boolean on) throws SocketException { + socket.setTcpNoDelay(on); + } + + @Override + public boolean getTcpNoDelay() throws SocketException { + return socket.getTcpNoDelay(); + } + + @Override + public void setSoLinger(boolean on, int linger) throws SocketException { + socket.setSoLinger(on, linger); + } + + @Override + public int getSoLinger() throws SocketException { + return socket.getSoLinger(); + } + + @Override + public void sendUrgentData(int data) throws IOException { + socket.sendUrgentData(data); + } + + @Override + public void setOOBInline(boolean on) throws SocketException { + socket.setOOBInline(on); + } + + @Override + public boolean getOOBInline() throws SocketException { + return socket.getOOBInline(); + } + + @Override + public void setSoTimeout(int timeout) throws SocketException { + socket.setSoTimeout(timeout); + } + + @Override + public int getSoTimeout() throws SocketException { + return socket.getSoTimeout(); + } + + @Override + public void setSendBufferSize(int size) throws SocketException { + socket.setSendBufferSize(size); + } + + @Override + public int getSendBufferSize() throws SocketException { + return socket.getSendBufferSize(); + } + + @Override + public void setReceiveBufferSize(int size) throws SocketException { + socket.setReceiveBufferSize(size); + } + + @Override + public int getReceiveBufferSize() throws SocketException { + return socket.getReceiveBufferSize(); + } + + @Override + public void setKeepAlive(boolean on) throws SocketException { + socket.setKeepAlive(on); + } + + @Override + public boolean getKeepAlive() throws SocketException { + return socket.getKeepAlive(); + } + + @Override + public void setTrafficClass(int tc) throws SocketException { + socket.setTrafficClass(tc); + } + + @Override + public int getTrafficClass() throws SocketException { + return socket.getTrafficClass(); + } + + @Override + public void setReuseAddress(boolean on) throws SocketException { + socket.setReuseAddress(on); + } + + @Override + public boolean getReuseAddress() throws SocketException { + return socket.getReuseAddress(); + } + + @Override + public void close() throws IOException { + try { + socket.close(); + } finally { + ses.shutdownNow(); + } + } + + @Override + public void shutdownInput() throws IOException { + socket.shutdownInput(); + } + + @Override + public void shutdownOutput() throws IOException { + socket.shutdownOutput(); + } + + @Override + public String toString() { + return socket.toString(); + } + + @Override + public boolean isConnected() { + return socket.isConnected(); + } + + @Override + public boolean isBound() { + return socket.isBound(); + } + + @Override + public boolean isClosed() { + return socket.isClosed(); + } + + @Override + public boolean isInputShutdown() { + return socket.isInputShutdown(); + } + + @Override + public boolean isOutputShutdown() { + return socket.isOutputShutdown(); + } + + /* + * The following three methods were added to java.net.Socket in Java SE 9. + * Since they're not supported on Android, and since we know that we + * never use them in Jakarta Mail, we just stub them out here. + */ + //@Override + public Socket setOption(SocketOption so, T val) throws IOException { + // socket.setOption(so, val); + // return this; + throw new UnsupportedOperationException("WriteTimeoutSocket.setOption"); + } + + //@Override + public T getOption(SocketOption so) throws IOException { + // return socket.getOption(so); + throw new UnsupportedOperationException("WriteTimeoutSocket.getOption"); + } + + //@Override + public Set> supportedOptions() { + // return socket.supportedOptions(); + return Collections.emptySet(); + } + + /** + * KLUDGE for Android, which has this illegal non-Java Compatible method. + * + * @return the FileDescriptor object + */ + public FileDescriptor getFileDescriptor$() { + try { + Method m = Socket.class.getDeclaredMethod("getFileDescriptor$"); + return (FileDescriptor)m.invoke(socket); + } catch (Exception ex) { + return null; + } + } +} + + +/** + * An OutputStream that wraps the Socket's OutputStream and uses + * the ScheduledExecutorService to schedule a task to close the + * socket (aborting the write) if the timeout expires. + */ +class TimeoutOutputStream extends OutputStream { + private final OutputStream os; + private final ScheduledExecutorService ses; + private final Callable timeoutTask; + private final int timeout; + private byte[] b1; + + public TimeoutOutputStream(OutputStream os0, ScheduledExecutorService ses, + int timeout) throws IOException { + this.os = os0; + this.ses = ses; + this.timeout = timeout; + timeoutTask = new Callable() { + @Override + public Object call() throws Exception { + os.close(); // close the stream to abort the write + return null; + } + }; + } + + @Override + public synchronized void write(int b) throws IOException { + if (b1 == null) + b1 = new byte[1]; + b1[0] = (byte)b; + this.write(b1); + } + + @Override + public synchronized void write(byte[] bs, int off, int len) + throws IOException { + if ((off < 0) || (off > bs.length) || (len < 0) || + ((off + len) > bs.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + + // Implement timeout with a scheduled task + ScheduledFuture sf = null; + try { + try { + if (timeout > 0) + sf = ses.schedule(timeoutTask, + timeout, TimeUnit.MILLISECONDS); + } catch (RejectedExecutionException ex) { + // ignore it; Executor was shut down by another thread, + // the following write should fail with IOException + } + os.write(bs, off, len); + } finally { + if (sf != null) + sf.cancel(true); + } + } + + @Override + public void close() throws IOException { + os.close(); + } +} diff --git a/app/src/main/java/com/sun/mail/util/logging/CollectorFormatter.java b/app/src/main/java/com/sun/mail/util/logging/CollectorFormatter.java new file mode 100644 index 0000000000..d2d889c215 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/logging/CollectorFormatter.java @@ -0,0 +1,603 @@ +/* + * Copyright (c) 2013, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2019 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import static com.sun.mail.util.logging.LogManagerProperties.fromLogManager; +import java.lang.reflect.UndeclaredThrowableException; +import java.text.MessageFormat; +import java.util.Comparator; +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.LogRecord; + +/** + * A LogRecord formatter that takes a sequence of LogRecords and combines them + * into a single summary result. Formating of the head, LogRecord, and tail are + * delegated to the wrapped formatter. + * + *

+ * By default each CollectorFormatter is initialized using the + * following LogManager configuration properties where + * <formatter-name> refers to the fully qualified class name + * or the fully qualified derived class name of the formatter. If properties + * are not defined, or contain invalid values, then the specified default values + * are used. + *

    + *
  • <formatter-name>.comparator name of a + * {@linkplain java.util.Comparator} class used to choose the collected + * LogRecord. If a comparator is specified then the max + * LogRecord is chosen. If comparator is set to the string literal + * null, then the last record is chosen. (defaults to + * {@linkplain SeverityComparator}) + * + *
  • <formatter-name>.comparator.reverse a boolean + * true to collect the min LogRecord or + * false to collect the max LogRecord. + * (defaults to false) + * + *
  • <formatter-name>.format the + * {@linkplain java.text.MessageFormat MessageFormat} string used to format the + * collected summary statistics. The arguments are explained in detail in the + * {@linkplain #getTail(java.util.logging.Handler) getTail} documentation. + * (defaults to {0}{1}{2}{4,choice,-1#|0#|0<... {4,number,integer} + * more}\n) + * + *
  • <formatter-name>.formatter name of a Formatter class + * used to format the collected LogRecord. + * (defaults to {@linkplain CompactFormatter}) + * + *
+ * + * @author Jason Mehrens + * @since JavaMail 1.5.2 + */ +public class CollectorFormatter extends Formatter { + /** + * Avoid depending on JMX runtime bean to get the start time. + */ + private static final long INIT_TIME = System.currentTimeMillis(); + /** + * The message format string used as the formatted output. + */ + private final String fmt; + /** + * The formatter used to format the chosen log record. + */ + private final Formatter formatter; + /** + * The comparator used to pick the log record to format. + */ + private final Comparator comparator; + /** + * The last accepted record. Synchronized access is preferred over volatile + * for this class. + */ + private LogRecord last; + /** + * The number of log records that have been formatted. + */ + private long count; + /** + * The number of log produced containing at least one log record. + * Only incremented when this formatter is reset. + */ + private long generation = 1L; + /** + * The number of log records that have been formatted with a thrown object. + */ + private long thrown; + /** + * The eldest log record time or eldest time possible for this instance. + */ + private long minMillis = INIT_TIME; + /** + * The newest log record time. + */ + private long maxMillis = Long.MIN_VALUE; + + /** + * Creates the formatter using the LogManager defaults. + * + * @throws SecurityException if a security manager exists and the caller + * does not have LoggingPermission("control"). + * @throws UndeclaredThrowableException if there are problems loading from + * the LogManager. + */ + public CollectorFormatter() { + final String p = getClass().getName(); + this.fmt = initFormat(p); + this.formatter = initFormatter(p); + this.comparator = initComparator(p); + } + + /** + * Creates the formatter using the given format. + * + * @param format the message format or null to use the LogManager default. + * @throws SecurityException if a security manager exists and the caller + * does not have LoggingPermission("control"). + * @throws UndeclaredThrowableException if there are problems loading from + * the LogManager. + */ + public CollectorFormatter(String format) { + final String p = getClass().getName(); + this.fmt = format == null ? initFormat(p) : format; + this.formatter = initFormatter(p); + this.comparator = initComparator(p); + } + + /** + * Creates the formatter using the given values. + * + * @param format the format string or null to use the LogManager default. + * @param f the formatter used on the collected log record or null to + * specify no formatter. + * @param c the comparator used to determine which log record to format or + * null to specify no comparator. + * @throws SecurityException if a security manager exists and the caller + * does not have LoggingPermission("control"). + * @throws UndeclaredThrowableException if there are problems loading from + * the LogManager. + */ + public CollectorFormatter(String format, Formatter f, + Comparator c) { + final String p = getClass().getName(); + this.fmt = format == null ? initFormat(p) : format; + this.formatter = f; + this.comparator = c; + } + + /** + * Accumulates log records which will be used to produce the final output. + * The output is generated using the {@link #getTail} method which also + * resets this formatter back to its original state. + * + * @param record the record to store. + * @return an empty string. + * @throws NullPointerException if the given record is null. + */ + @Override + public String format(final LogRecord record) { + if (record == null) { + throw new NullPointerException(); + } + + boolean accepted; + do { + final LogRecord peek = peek(); + //The self compare of the first record acts like a type check. + LogRecord update = apply(peek != null ? peek : record, record); + if (peek != update) { //Not identical. + update.getSourceMethodName(); //Infer caller, null check. + accepted = acceptAndUpdate(peek, update); + } else { + accepted = accept(peek, record); + } + } while (!accepted); + return ""; + } + + /** + * Formats the collected LogRecord and summary statistics. The collected + * results are reset after calling this method. The + * {@linkplain java.text.MessageFormat java.text} argument indexes are + * assigned to the following properties: + * + *
    + *
  1. {@code head} the + * {@linkplain Formatter#getHead(java.util.logging.Handler) head} string + * returned from the target formatter and + * {@linkplain #finish(java.lang.String) finished} by this formatter. + *
  2. {@code formatted} the current log record + * {@linkplain Formatter#format(java.util.logging.LogRecord) formatted} by + * the target formatter and {@linkplain #finish(java.lang.String) finished} + * by this formatter. If the formatter is null then record is formatted by + * this {@linkplain #formatMessage(java.util.logging.LogRecord) formatter}. + *
  3. {@code tail} the + * {@linkplain Formatter#getTail(java.util.logging.Handler) tail} string + * returned from the target formatter and + * {@linkplain #finish(java.lang.String) finished} by this formatter. + *
  4. {@code count} the total number of log records + * {@linkplain #format consumed} by this formatter. + *
  5. {@code remaining} the count minus one. + *
  6. {@code thrown} the total number of log records + * {@linkplain #format consumed} by this formatter with an assigned + * {@linkplain java.util.logging.LogRecord#getThrown throwable}. + *
  7. {@code normal messages} the count minus the thrown. + *
  8. {@code minMillis} the eldest log record + * {@linkplain java.util.logging.LogRecord#getMillis event time} + * {@linkplain #format consumed} by this formatter. If the count is zero + * then this is set to the previous max or approximate start time if there + * was no previous max. By default this parameter is defined as a number. + * The format type and format style rules from the + * {@linkplain java.text.MessageFormat} should be used to convert this from + * milliseconds to a date or time. + *
  9. {@code maxMillis} the most recent log record + * {@linkplain java.util.logging.LogRecord#getMillis event time} + * {@linkplain #format consumed} by this formatter. If the count is zero + * then this is set to the {@linkplain System#currentTimeMillis() current time}. + * By default this parameter is defined as a number. The format type and + * format style rules from the {@linkplain java.text.MessageFormat} should + * be used to convert this from milliseconds to a date or time. + *
  10. {@code elapsed} the elapsed time in milliseconds between the + * {@code maxMillis} and {@code minMillis}. + *
  11. {@code startTime} the approximate start time in milliseconds. By + * default this parameter is defined as a number. The format type and format + * style rules from the {@linkplain java.text.MessageFormat} should be used + * to convert this from milliseconds to a date or time. + *
  12. {@code currentTime} the + * {@linkplain System#currentTimeMillis() current time} in milliseconds. By + * default this parameter is defined as a number. The format type and format + * style rules from the {@linkplain java.text.MessageFormat} should be used + * to convert this from milliseconds to a date or time. + *
  13. {@code uptime} the elapsed time in milliseconds between the + * {@code currentTime} and {@code startTime}. + *
  14. {@code generation} the number times this method produced output with + * at least one {@linkplain #format consumed} log record. This can be used + * to track the number of complete reports this formatter has produced. + *
+ * + *

+ * Some example formats:
+ *

    + *
  • {@code com.sun.mail.util.logging.CollectorFormatter.format={0}{1}{2}{4,choice,-1#|0#|0<... {4,number,integer} more}\n} + *

    + * This prints the head ({@code {0}}), format ({@code {1}}), and tail + * ({@code {2}}) from the target formatter followed by the number of + * remaining ({@code {4}}) log records consumed by this formatter if there + * are any remaining records. + *

    +     * Encoding failed.|NullPointerException: null String.getBytes(:913)... 3 more
    +     * 
    + *
  • {@code com.sun.mail.util.logging.CollectorFormatter.format=These {3} messages occurred between\n{7,date,EEE, MMM dd HH:mm:ss:S ZZZ yyyy} and {8,time,EEE, MMM dd HH:mm:ss:S ZZZ yyyy}\n} + *

    + * This prints the count ({@code {3}}) followed by the date and time of the + * eldest log record ({@code {7}}) and the date and time of the most recent + * log record ({@code {8}}). + *

    +     * These 292 messages occurred between
    +     * Tue, Jul 21 14:11:42:449 -0500 2009 and Fri, Nov 20 07:29:24:0 -0600 2009
    +     * 
    + *
  • {@code com.sun.mail.util.logging.CollectorFormatter.format=These {3} messages occurred between {9,choice,86400000#{7,date} {7,time} and {8,time}|86400000<{7,date} and {8,date}}\n} + *

    + * This prints the count ({@code {3}}) and then chooses the format based on + * the elapsed time ({@code {9}}). If the elapsed time is less than one day + * then the eldest log record ({@code {7}}) date and time is formatted + * followed by just the time of the most recent log record ({@code {8}}. + * Otherwise, the just the date of the eldest log record ({@code {7}}) and + * just the date of most recent log record ({@code {8}} is formatted. + *

    +     * These 73 messages occurred between Jul 21, 2009 2:11:42 PM and 2:13:32 PM
    +     *
    +     * These 116 messages occurred between Jul 21, 2009 and Aug 20, 2009
    +     * 
    + *
  • {@code com.sun.mail.util.logging.CollectorFormatter.format={13} alert reports since {10,date}.\n} + *

    + * This prints the generation ({@code {13}}) followed by the start time + * ({@code {10}}) formatted as a date. + *

    +     * 4,320 alert reports since Jul 21, 2012.
    +     * 
    + *
+ * + * @param h the handler or null. + * @return the output string. + */ + @Override + public String getTail(final Handler h) { + super.getTail(h); //Be forward compatible with super.getHead. + return formatRecord(h, true); + } + + /** + * Formats the collected LogRecord and summary statistics. The LogRecord and + * summary statistics are not changed by calling this method. + * + * @return the current record formatted or the default toString. + * @see #getTail(java.util.logging.Handler) + */ + @Override + public String toString() { + String result; + try { + result = formatRecord((Handler) null, false); + } catch (final RuntimeException ignore) { + result = super.toString(); + } + return result; + } + + /** + * Used to choose the collected LogRecord. This implementation returns the + * greater of two LogRecords. + * + * @param t the current record. + * @param u the record that could replace the current. + * @return the greater of the given log records. + * @throws NullPointerException may occur if either record is null. + */ + protected LogRecord apply(final LogRecord t, final LogRecord u) { + if (t == null || u == null) { + throw new NullPointerException(); + } + + if (comparator != null) { + return comparator.compare(t, u) >= 0 ? t : u; + } else { + return u; + } + } + + /** + * Updates the summary statistics only if the expected record matches the + * last record. The update record is not stored. + * + * @param e the LogRecord that is expected. + * @param u the LogRecord used to collect statistics. + * @return true if the last record was the expected record. + * @throws NullPointerException if the update record is null. + */ + private synchronized boolean accept(final LogRecord e, final LogRecord u) { + /** + * LogRecord methods must be called before the check of the last stored + * record to guard against subclasses of LogRecord that might attempt to + * reset the state by triggering a call to getTail. + */ + final long millis = u.getMillis(); //Null check. + final Throwable ex = u.getThrown(); + if (last == e) { //Only if the exact same reference. + if (++count != 1L) { + minMillis = Math.min(minMillis, millis); + } else { //Show single records as instant and not a time period. + minMillis = millis; + } + maxMillis = Math.max(maxMillis, millis); + + if (ex != null) { + ++thrown; + } + return true; + } else { + return false; + } + } + + /** + * Resets all of the collected summary statistics including the LogRecord. + * @param min the current min milliseconds. + */ + private synchronized void reset(final long min) { + if (last != null) { + last = null; + ++generation; + } + + count = 0L; + thrown = 0L; + minMillis = min; + maxMillis = Long.MIN_VALUE; + } + + /** + * Formats the given record with the head and tail. + * + * @param h the Handler or null. + * @param reset true if the summary statistics and LogRecord should be reset + * back to initial values. + * @return the formatted string. + * @see #getTail(java.util.logging.Handler) + */ + private String formatRecord(final Handler h, final boolean reset) { + final LogRecord record; + final long c; + final long t; + final long g; + long msl; + long msh; + long now; + synchronized (this) { + record = last; + c = count; + g = generation; + t = thrown; + msl = minMillis; + msh = maxMillis; + now = System.currentTimeMillis(); + if (c == 0L) { + msh = now; + } + + if (reset) { //BUG ID 6351685 + reset(msh); + } + } + + final String head; + final String msg; + final String tail; + final Formatter f = this.formatter; + if (f != null) { + synchronized (f) { + head = f.getHead(h); + msg = record != null ? f.format(record) : ""; + tail = f.getTail(h); + } + } else { + head = ""; + msg = record != null ? formatMessage(record) : ""; + tail = ""; + } + + Locale l = null; + if (record != null) { + ResourceBundle rb = record.getResourceBundle(); + l = rb == null ? null : rb.getLocale(); + } + + final MessageFormat mf; + if (l == null) { //BUG ID 8039165 + mf = new MessageFormat(fmt); + } else { + mf = new MessageFormat(fmt, l); + } + + /** + * These arguments are described in the getTail documentation. + */ + return mf.format(new Object[]{finish(head), finish(msg), finish(tail), + c, (c - 1L), t, (c - t), msl, msh, (msh - msl), INIT_TIME, now, + (now - INIT_TIME), g}); + } + + /** + * Applied to the head, format, and tail returned by the target formatter. + * This implementation trims all input strings. + * + * @param s the string to transform. + * @return the transformed string. + * @throws NullPointerException if the given string is null. + */ + protected String finish(String s) { + return s.trim(); + } + + /** + * Peek at the current log record. + * + * @return null or the current log record. + */ + private synchronized LogRecord peek() { + return this.last; + } + + /** + * Updates the summary statistics and stores given LogRecord if the expected + * record matches the current record. + * + * @param e the expected record. + * @param u the update record. + * @return true if the update was performed. + * @throws NullPointerException if the update record is null. + */ + private synchronized boolean acceptAndUpdate(LogRecord e, LogRecord u) { + if (accept(e, u)) { + this.last = u; + return true; + } else { + return false; + } + } + + /** + * Gets the message format string from the LogManager or creates the default + * message format string. + * + * @param p the class name prefix. + * @return the format string. + * @throws NullPointerException if the given argument is null. + */ + private String initFormat(final String p) { + String v = fromLogManager(p.concat(".format")); + if (v == null || v.length() == 0) { + v = "{0}{1}{2}{4,choice,-1#|0#|0<... {4,number,integer} more}\n"; + } + return v; + } + + /** + * Gets and creates the formatter from the LogManager or creates the default + * formatter. + * + * @param p the class name prefix. + * @return the formatter. + * @throws NullPointerException if the given argument is null. + * @throws UndeclaredThrowableException if the formatter can not be created. + */ + private Formatter initFormatter(final String p) { + Formatter f; + String v = fromLogManager(p.concat(".formatter")); + if (v != null && v.length() != 0) { + if (!"null".equalsIgnoreCase(v)) { + try { + f = LogManagerProperties.newFormatter(v); + } catch (final RuntimeException re) { + throw re; + } catch (final Exception e) { + throw new UndeclaredThrowableException(e); + } + } else { + f = null; + } + } else { + //Don't force the byte code verifier to load the formatter. + f = Formatter.class.cast(new CompactFormatter()); + } + return f; + } + + /** + * Gets and creates the comparator from the LogManager or returns the + * default comparator. + * + * @param p the class name prefix. + * @return the comparator or null. + * @throws IllegalArgumentException if it was specified that the comparator + * should be reversed but no initial comparator was specified. + * @throws NullPointerException if the given argument is null. + * @throws UndeclaredThrowableException if the comparator can not be + * created. + */ + @SuppressWarnings("unchecked") + private Comparator initComparator(final String p) { + Comparator c; + final String name = fromLogManager(p.concat(".comparator")); + final String reverse = fromLogManager(p.concat(".comparator.reverse")); + try { + if (name != null && name.length() != 0) { + if (!"null".equalsIgnoreCase(name)) { + c = LogManagerProperties.newComparator(name); + if (Boolean.parseBoolean(reverse)) { + assert c != null; + c = LogManagerProperties.reverseOrder(c); + } + } else { + if (reverse != null) { + throw new IllegalArgumentException( + "No comparator to reverse."); + } else { + c = null; //No ordering. + } + } + } else { + if (reverse != null) { + throw new IllegalArgumentException( + "No comparator to reverse."); + } else { + //Don't force the byte code verifier to load the comparator. + c = Comparator.class.cast(SeverityComparator.getInstance()); + } + } + } catch (final RuntimeException re) { + throw re; //Avoid catch all. + } catch (final Exception e) { + throw new UndeclaredThrowableException(e); + } + return c; + } +} diff --git a/app/src/main/java/com/sun/mail/util/logging/CompactFormatter.java b/app/src/main/java/com/sun/mail/util/logging/CompactFormatter.java new file mode 100644 index 0000000000..7a1a2fb961 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/logging/CompactFormatter.java @@ -0,0 +1,865 @@ +/* + * Copyright (c) 2013, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2019 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import java.util.*; +import java.util.logging.LogRecord; + +/** + * A plain text formatter that can produce fixed width output. By default this + * formatter will produce output no greater than 160 characters wide plus the + * separator and newline characters. Only specified fields support an + * {@linkplain #toAlternate(java.lang.String) alternate} fixed width format. + *

+ * By default each CompactFormatter is initialized using the + * following LogManager configuration properties where + * <formatter-name> refers to the fully qualified class name + * or the fully qualified derived class name of the formatter. If properties are + * not defined, or contain invalid values, then the specified default values are + * used. + *

    + *
  • <formatter-name>.format - the {@linkplain java.util.Formatter + * format} string used to transform the output. The format string can be + * used to fix the output size. (defaults to %7$#.160s%n)
  • + *
+ * + * @author Jason Mehrens + * @since JavaMail 1.5.2 + */ +public class CompactFormatter extends java.util.logging.Formatter { + + /** + * Load any declared classes to workaround GLASSFISH-21258. + */ + static { + loadDeclaredClasses(); + } + + /** + * Used to load declared classes encase class loader doesn't allow loading + * during JVM termination. This method is used with unit testing. + * + * @return an array of classes never null. + */ + private static Class[] loadDeclaredClasses() { + return new Class[]{Alternate.class}; + } + + /** + * Holds the java.util.Formatter pattern. + */ + private final String fmt; + + /** + * Creates an instance with a default format pattern. + */ + public CompactFormatter() { + String p = getClass().getName(); + this.fmt = initFormat(p); + } + + /** + * Creates an instance with the given format pattern. + * + * @param format the {@linkplain java.util.Formatter pattern} or null to use + * the LogManager default. The arguments are described in the + * {@linkplain #format(java.util.logging.LogRecord) format} method. + */ + public CompactFormatter(final String format) { + String p = getClass().getName(); + this.fmt = format == null ? initFormat(p) : format; + } + + /** + * Format the given log record and returns the formatted string. The + * {@linkplain java.util.Formatter#format(java.lang.String, java.lang.Object...) + * java.util} argument indexes are assigned to the following properties: + * + *
    + *
  1. {@code format} - the {@linkplain java.util.Formatter + * java.util.Formatter} format string specified in the + * <formatter-name>.format property or the format that was given when + * this formatter was created.
  2. + *
  3. {@code date} - if the log record supports nanoseconds then a + * ZonedDateTime object representing the event time of the log record in the + * system time zone. Otherwise, a {@linkplain Date} object representing + * {@linkplain LogRecord#getMillis event time} of the log record.
  4. + *
  5. {@code source} - a string representing the caller, if available; + * otherwise, the logger's name.
  6. + *
  7. {@code logger} - the logger's + * {@linkplain Class#getSimpleName() simple} + * {@linkplain LogRecord#getLoggerName() name}.
  8. + *
  9. {@code level} - the + * {@linkplain java.util.logging.Level#getLocalizedName log level}.
  10. + *
  11. {@code message} - the formatted log message returned from the + * {@linkplain #formatMessage(LogRecord)} method.
  12. + *
  13. {@code thrown} - a string representing the + * {@linkplain LogRecord#getThrown throwable} associated with the log record + * and a relevant stack trace element if available; otherwise, an empty + * string is used.
  14. + *
  15. {@code message|thrown} The message and the thrown properties joined + * as one parameter. This parameter supports + * {@linkplain #toAlternate(java.lang.String) alternate} form.
  16. + *
  17. {@code thrown|message} The thrown and message properties joined as + * one parameter. This parameter supports + * {@linkplain #toAlternate(java.lang.String) alternate} form.
  18. + *
  19. {@code sequence} the + * {@linkplain LogRecord#getSequenceNumber() sequence number} if the given + * log record.
  20. + *
  21. {@code thread id} the {@linkplain LogRecord#getThreadID() thread id} + * of the given log record. By default this is formatted as a {@code long} + * by an unsigned conversion.
  22. + *
  23. {@code error} the throwable + * {@linkplain Class#getSimpleName() simple class name} and + * {@linkplain #formatError(LogRecord) error message} without any stack + * trace.
  24. + *
  25. {@code message|error} The message and error properties joined as one + * parameter. This parameter supports + * {@linkplain #toAlternate(java.lang.String) alternate} form.
  26. + *
  27. {@code error|message} The error and message properties joined as one + * parameter. This parameter supports + * {@linkplain #toAlternate(java.lang.String) alternate} form.
  28. + *
  29. {@code backtrace} only the + * {@linkplain #formatBackTrace(LogRecord) stack trace} of the given + * throwable.
  30. + *
  31. {@code bundlename} the resource bundle + * {@linkplain LogRecord#getResourceBundleName() name} of the given log + * record.
  32. + *
  33. {@code key} the {@linkplain LogRecord#getMessage() raw message} + * before localization or formatting.
  34. + *
+ * + *

+ * Some example formats:
+ *

    + *
  • {@code com.sun.mail.util.logging.CompactFormatter.format=%7$#.160s%n} + *

    + * This prints only 160 characters of the message|thrown ({@code 7$}) using + * the {@linkplain #toAlternate(java.lang.String) alternate} form. The + * separator is not included as part of the total width. + *

    +     * Encoding failed.|NullPointerException: null String.getBytes(:913)
    +     * 
    + * + *
  • {@code com.sun.mail.util.logging.CompactFormatter.format=%7$#.20s%n} + *

    + * This prints only 20 characters of the message|thrown ({@code 7$}) using + * the {@linkplain #toAlternate(java.lang.String) alternate} form. This will + * perform a weighted truncation of both the message and thrown properties + * of the log record. The separator is not included as part of the total + * width. + *

    +     * Encoding|NullPointerE
    +     * 
    + * + *
  • {@code com.sun.mail.util.logging.CompactFormatter.format=%1$tc %2$s%n%4$s: %5$s%6$s%n} + *

    + * This prints the timestamp ({@code 1$}) and the source ({@code 2$}) on the + * first line. The second line is the log level ({@code 4$}), log message + * ({@code 5$}), and the throwable with a relevant stack trace element + * ({@code 6$}) if one is available. + *

    +     * Fri Nov 20 07:29:24 CST 2009 MyClass fatal
    +     * SEVERE: Encoding failed.NullPointerException: null String.getBytes(:913)
    +     * 
    + * + *
  • {@code com.sun.mail.util.logging.CompactFormatter.format=%4$s: %12$#.160s%n} + *

    + * This prints the log level ({@code 4$}) and only 160 characters of the + * message|error ({@code 12$}) using the alternate form. + *

    +     * SEVERE: Unable to send notification.|SocketException: Permission denied: connect
    +     * 
    + * + *
  • {@code com.sun.mail.util.logging.CompactFormatter.format=[%9$d][%1$tT][%10$d][%2$s] %5$s%n%6$s%n} + *

    + * This prints the sequence ({@code 9$}), event time ({@code 1$}) as 24 hour + * time, thread id ({@code 10$}), source ({@code 2$}), log message + * ({@code 5$}), and the throwable with back trace ({@code 6$}). + *

    +     * [125][14:11:42][38][MyClass fatal] Unable to send notification.
    +     * SocketException: Permission denied: connect SMTPTransport.openServer(:1949)
    +     * 
    + * + *
+ * + * @param record the log record to format. + * @return the formatted record. + * @throws NullPointerException if the given record is null. + */ + @Override + public String format(final LogRecord record) { + //LogRecord is mutable so define local vars. + ResourceBundle rb = record.getResourceBundle(); + Locale l = rb == null ? null : rb.getLocale(); + + String msg = formatMessage(record); + String thrown = formatThrown(record); + String err = formatError(record); + Object[] params = { + formatZonedDateTime(record), + formatSource(record), + formatLoggerName(record), + formatLevel(record), + msg, + thrown, + new Alternate(msg, thrown), + new Alternate(thrown, msg), + record.getSequenceNumber(), + formatThreadID(record), + err, + new Alternate(msg, err), + new Alternate(err, msg), + formatBackTrace(record), + record.getResourceBundleName(), + record.getMessage()}; + + if (l == null) { //BUG ID 6282094 + return String.format(fmt, params); + } else { + return String.format(l, fmt, params); + } + } + + /** + * Formats message for the log record. This method removes any fully + * qualified throwable class names from the message. + * + * @param record the log record. + * @return the formatted message string. + */ + @Override + public String formatMessage(final LogRecord record) { + String msg = super.formatMessage(record); + msg = replaceClassName(msg, record.getThrown()); + msg = replaceClassName(msg, record.getParameters()); + return msg; + } + + /** + * Formats the message from the thrown property of the log record. This + * method replaces fully qualified throwable class names from the message + * cause chain with simple class names. + * + * @param t the throwable to format or null. + * @return the empty string if null was given or the formatted message + * string from the throwable which may be null. + */ + public String formatMessage(final Throwable t) { + String r; + if (t != null) { + final Throwable apply = apply(t); + final String m = apply.getLocalizedMessage(); + final String s = apply.toString(); + final String sn = simpleClassName(apply.getClass()); + if (!isNullOrSpaces(m)) { + if (s.contains(m)) { + if (s.startsWith(apply.getClass().getName()) + || s.startsWith(sn)) { + r = replaceClassName(m, t); + } else { + r = replaceClassName(simpleClassName(s), t); + } + } else { + r = replaceClassName(simpleClassName(s) + ": " + m, t); + } + } else { + r = replaceClassName(simpleClassName(s), t); + } + + if (!r.contains(sn)) { + r = sn + ": " + r; + } + } else { + r = ""; + } + return r; + } + + /** + * Formats the level property of the given log record. + * + * @param record the record. + * @return the formatted logger name. + * @throws NullPointerException if the given record is null. + */ + public String formatLevel(final LogRecord record) { + return record.getLevel().getLocalizedName(); + } + + /** + * Formats the source from the given log record. + * + * @param record the record. + * @return the formatted source of the log record. + * @throws NullPointerException if the given record is null. + */ + public String formatSource(final LogRecord record) { + String source = record.getSourceClassName(); + if (source != null) { + if (record.getSourceMethodName() != null) { + source = simpleClassName(source) + " " + + record.getSourceMethodName(); + } else { + source = simpleClassName(source); + } + } else { + source = simpleClassName(record.getLoggerName()); + } + return source; + } + + /** + * Formats the logger name property of the given log record. + * + * @param record the record. + * @return the formatted logger name. + * @throws NullPointerException if the given record is null. + */ + public String formatLoggerName(final LogRecord record) { + return simpleClassName(record.getLoggerName()); + } + + /** + * Formats the thread id property of the given log record. By default this + * is formatted as a {@code long} by an unsigned conversion. + * + * @param record the record. + * @return the formatted thread id as a number. + * @throws NullPointerException if the given record is null. + * @since JavaMail 1.5.4 + */ + public Number formatThreadID(final LogRecord record) { + /** + * Thread.getID is defined as long and LogRecord.getThreadID is defined + * as int. Convert to unsigned as a means to better map the two types of + * thread identifiers. + */ + return (((long) record.getThreadID()) & 0xffffffffL); + } + + /** + * Formats the thrown property of a LogRecord. The returned string will + * contain a throwable message with a back trace. + * + * @param record the record. + * @return empty string if nothing was thrown or formatted string. + * @throws NullPointerException if the given record is null. + * @see #apply(java.lang.Throwable) + * @see #formatBackTrace(java.util.logging.LogRecord) + */ + public String formatThrown(final LogRecord record) { + String msg; + final Throwable t = record.getThrown(); + if (t != null) { + String site = formatBackTrace(record); + msg = formatMessage(t) + (isNullOrSpaces(site) ? "" : ' ' + site); + } else { + msg = ""; + } + return msg; + } + + /** + * Formats the thrown property of a LogRecord as an error message. The + * returned string will not contain a back trace. + * + * @param record the record. + * @return empty string if nothing was thrown or formatted string. + * @throws NullPointerException if the given record is null. + * @see Throwable#toString() + * @see #apply(java.lang.Throwable) + * @see #formatMessage(java.lang.Throwable) + * @since JavaMail 1.5.4 + */ + public String formatError(final LogRecord record) { + return formatMessage(record.getThrown()); + } + + /** + * Formats the back trace for the given log record. + * + * @param record the log record to format. + * @return the formatted back trace. + * @throws NullPointerException if the given record is null. + * @see #apply(java.lang.Throwable) + * @see #formatThrown(java.util.logging.LogRecord) + * @see #ignore(java.lang.StackTraceElement) + */ + public String formatBackTrace(final LogRecord record) { + String site = ""; + final Throwable t = record.getThrown(); + if (t != null) { + final Throwable root = apply(t); + StackTraceElement[] trace = root.getStackTrace(); + site = findAndFormat(trace); + if (isNullOrSpaces(site)) { + int limit = 0; + for (Throwable c = t; c != null; c = c.getCause()) { + StackTraceElement[] ste = c.getStackTrace(); + site = findAndFormat(ste); + if (!isNullOrSpaces(site)) { + break; + } else { + if (trace.length == 0) { + trace = ste; + } + } + + //Deal with excessive cause chains + //and cyclic throwables. + if (++limit == (1 << 16)) { + break; //Give up. + } + } + + //Punt. + if (isNullOrSpaces(site) && trace.length != 0) { + site = formatStackTraceElement(trace[0]); + } + } + } + return site; + } + + /** + * Finds and formats the first stack frame of interest. + * + * @param trace the fill stack to examine. + * @return a String that best describes the call site. + * @throws NullPointerException if stack trace element array is null. + */ + private String findAndFormat(final StackTraceElement[] trace) { + String site = ""; + for (StackTraceElement s : trace) { + if (!ignore(s)) { + site = formatStackTraceElement(s); + break; + } + } + + //Check if all code was compiled with no debugging info. + if (isNullOrSpaces(site)) { + for (StackTraceElement s : trace) { + if (!defaultIgnore(s)) { + site = formatStackTraceElement(s); + break; + } + } + } + return site; + } + + /** + * Formats a stack trace element into a simple call site. + * + * @param s the stack trace element to format. + * @return the formatted stack trace element. + * @throws NullPointerException if stack trace element is null. + * @see #formatThrown(java.util.logging.LogRecord) + */ + private String formatStackTraceElement(final StackTraceElement s) { + String v = simpleClassName(s.getClassName()); + String result; + if (v != null) { + result = s.toString().replace(s.getClassName(), v); + } else { + result = s.toString(); + } + + //If the class name contains the simple file name then remove file name. + v = simpleFileName(s.getFileName()); + if (v != null && result.startsWith(v)) { + result = result.replace(s.getFileName(), ""); + } + return result; + } + + /** + * Chooses a single throwable from the cause chain that will be formatted. + * This implementation chooses the throwable that best describes the chain. + * Subclasses can override this method to choose an alternate throwable for + * formatting. + * + * @param t the throwable from the log record. + * @return the chosen throwable or null only if the given argument is null. + * @see #formatThrown(java.util.logging.LogRecord) + */ + protected Throwable apply(final Throwable t) { + return SeverityComparator.getInstance().apply(t); + } + + /** + * Determines if a stack frame should be ignored as the cause of an error. + * + * @param s the stack trace element. + * @return true if this frame should be ignored. + * @see #formatThrown(java.util.logging.LogRecord) + */ + protected boolean ignore(final StackTraceElement s) { + return isUnknown(s) || defaultIgnore(s); + } + + /** + * Defines the alternate format. This implementation removes all control + * characters from the given string. + * + * @param s any string or null. + * @return null if the argument was null otherwise, an alternate string. + */ + protected String toAlternate(final String s) { + return s != null ? s.replaceAll("[\\x00-\\x1F\\x7F]+", "") : null; + } + + /** + * Gets the zoned date time from the given log record. + * + * @param record the current log record. + * @return a zoned date time or a legacy date object. + * @throws NullPointerException if the given record is null. + * @since JavaMail 1.5.6 + */ + private Comparable formatZonedDateTime(final LogRecord record) { + Comparable zdt = LogManagerProperties.getZonedDateTime(record); + if (zdt == null) { + zdt = new java.util.Date(record.getMillis()); + } + return zdt; + } + + /** + * Determines if a stack frame should be ignored as the cause of an error. + * This does not check for unknown line numbers because code can be compiled + * without debugging info. + * + * @param s the stack trace element. + * @return true if this frame should be ignored. + */ + private boolean defaultIgnore(final StackTraceElement s) { + return isSynthetic(s) || isStaticUtility(s) || isReflection(s); + } + + /** + * Determines if a stack frame is for a static utility class. + * + * @param s the stack trace element. + * @return true if this frame should be ignored. + */ + private boolean isStaticUtility(final StackTraceElement s) { + try { + return LogManagerProperties.isStaticUtilityClass(s.getClassName()); + } catch (RuntimeException ignore) { + } catch (Exception | LinkageError ignore) { + } + final String cn = s.getClassName(); + return (cn.endsWith("s") && !cn.endsWith("es")) + || cn.contains("Util") || cn.endsWith("Throwables"); + } + + /** + * Determines if a stack trace element is for a synthetic method. + * + * @param s the stack trace element. + * @return true if synthetic. + * @throws NullPointerException if stack trace element is null. + */ + private boolean isSynthetic(final StackTraceElement s) { + return s.getMethodName().indexOf('$') > -1; + } + + /** + * Determines if a stack trace element has an unknown line number or a + * native line number. + * + * @param s the stack trace element. + * @return true if the line number is unknown. + * @throws NullPointerException if stack trace element is null. + */ + private boolean isUnknown(final StackTraceElement s) { + return s.getLineNumber() < 0; + } + + /** + * Determines if a stack trace element represents a reflection frame. + * + * @param s the stack trace element. + * @return true if the line number is unknown. + * @throws NullPointerException if stack trace element is null. + */ + private boolean isReflection(final StackTraceElement s) { + try { + return LogManagerProperties.isReflectionClass(s.getClassName()); + } catch (RuntimeException ignore) { + } catch (Exception | LinkageError ignore) { + } + return s.getClassName().startsWith("java.lang.reflect.") + || s.getClassName().startsWith("sun.reflect."); + } + + /** + * Creates the format pattern for this formatter. + * + * @param p the class name prefix. + * @return the java.util.Formatter format string. + * @throws NullPointerException if the given class name is null. + */ + private String initFormat(final String p) { + String v = LogManagerProperties.fromLogManager(p.concat(".format")); + if (isNullOrSpaces(v)) { + v = "%7$#.160s%n"; //160 chars split between message and thrown. + } + return v; + } + + /** + * Searches the given message for all instances fully qualified class name + * with simple class name based off of the types contained in the given + * parameter array. + * + * @param msg the message. + * @param t the throwable cause chain to search or null. + * @return the modified message string. + */ + private static String replaceClassName(String msg, Throwable t) { + if (!isNullOrSpaces(msg)) { + int limit = 0; + for (Throwable c = t; c != null; c = c.getCause()) { + final Class k = c.getClass(); + msg = msg.replace(k.getName(), simpleClassName(k)); + + //Deal with excessive cause chains and cyclic throwables. + if (++limit == (1 << 16)) { + break; //Give up. + } + } + } + return msg; + } + + /** + * Searches the given message for all instances fully qualified class name + * with simple class name based off of the types contained in the given + * parameter array. + * + * @param msg the message or null. + * @param p the parameter array or null. + * @return the modified message string. + */ + private static String replaceClassName(String msg, Object[] p) { + if (!isNullOrSpaces(msg) && p != null) { + for (Object o : p) { + if (o != null) { + final Class k = o.getClass(); + msg = msg.replace(k.getName(), simpleClassName(k)); + } + } + } + return msg; + } + + /** + * Gets the simple class name from the given class. This is a workaround for + * BUG ID JDK-8057919. + * + * @param k the class object. + * @return the simple class name or null. + * @since JavaMail 1.5.3 + */ + private static String simpleClassName(final Class k) { + try { + return k.getSimpleName(); + } catch (final InternalError JDK8057919) { + } + return simpleClassName(k.getName()); + } + + /** + * Converts a fully qualified class name to a simple class name. If the + * leading part of the given string is not a legal class name then the given + * string is returned. + * + * @param name the fully qualified class name prefix or null. + * @return the simple class name or given input. + */ + private static String simpleClassName(String name) { + if (name != null) { + int cursor = 0; + int sign = -1; + int dot = -1; + for (int c, prev = dot; cursor < name.length(); + cursor += Character.charCount(c)) { + c = name.codePointAt(cursor); + if (!Character.isJavaIdentifierPart(c)) { + if (c == ((int) '.')) { + if ((dot + 1) != cursor && (dot + 1) != sign) { + prev = dot; + dot = cursor; + } else { + return name; + } + } else { + if ((dot + 1) == cursor) { + dot = prev; + } + break; + } + } else { + if (c == ((int) '$')) { + sign = cursor; + } + } + } + + if (dot > -1 && ++dot < cursor && ++sign < cursor) { + name = name.substring(sign > dot ? sign : dot); + } + } + return name; + } + + /** + * Converts a file name with an extension to a file name without an + * extension. + * + * @param name the full file name or null. + * @return the simple file name or null. + */ + private static String simpleFileName(String name) { + if (name != null) { + final int index = name.lastIndexOf('.'); + name = index > -1 ? name.substring(0, index) : name; + } + return name; + } + + /** + * Determines is the given string is null or spaces. + * + * @param s the string or null. + * @return true if null or spaces. + */ + private static boolean isNullOrSpaces(final String s) { + return s == null || s.trim().length() == 0; + } + + /** + * Used to format two arguments as fixed length message. + */ + private class Alternate implements java.util.Formattable { + + /** + * The left side of the output. + */ + private final String left; + /** + * The right side of the output. + */ + private final String right; + + /** + * Creates an alternate output. + * + * @param left the left side or null. + * @param right the right side or null. + */ + Alternate(final String left, final String right) { + this.left = String.valueOf(left); + this.right = String.valueOf(right); + } + + @SuppressWarnings("override") //JDK-6954234 + public void formatTo(java.util.Formatter formatter, int flags, + int width, int precision) { + + String l = left; + String r = right; + if ((flags & java.util.FormattableFlags.UPPERCASE) + == java.util.FormattableFlags.UPPERCASE) { + l = l.toUpperCase(formatter.locale()); + r = r.toUpperCase(formatter.locale()); + } + + if ((flags & java.util.FormattableFlags.ALTERNATE) + == java.util.FormattableFlags.ALTERNATE) { + l = toAlternate(l); + r = toAlternate(r); + } + + if (precision <= 0) { + precision = Integer.MAX_VALUE; + } + + int fence = Math.min(l.length(), precision); + if (fence > (precision >> 1)) { + fence = Math.max(fence - r.length(), fence >> 1); + } + + if (fence > 0) { + if (fence > l.length() + && Character.isHighSurrogate(l.charAt(fence - 1))) { + --fence; + } + l = l.substring(0, fence); + } + r = r.substring(0, Math.min(precision - fence, r.length())); + + if (width > 0) { + final int half = width >> 1; + if (l.length() < half) { + l = pad(flags, l, half); + } + + if (r.length() < half) { + r = pad(flags, r, half); + } + } + + Object[] empty = Collections.emptySet().toArray(); + formatter.format(l, empty); + if (l.length() != 0 && r.length() != 0) { + formatter.format("|", empty); + } + formatter.format(r, empty); + } + + /** + * Pad the given input string. + * + * @param flags the formatter flags. + * @param s the string to pad. + * @param length the final string length. + * @return the padded string. + */ + private String pad(int flags, String s, int length) { + final int padding = length - s.length(); + final StringBuilder b = new StringBuilder(length); + if ((flags & java.util.FormattableFlags.LEFT_JUSTIFY) + == java.util.FormattableFlags.LEFT_JUSTIFY) { + for (int i = 0; i < padding; ++i) { + b.append('\u0020'); + } + b.append(s); + } else { + b.append(s); + for (int i = 0; i < padding; ++i) { + b.append('\u0020'); + } + } + return b.toString(); + } + } +} diff --git a/app/src/main/java/com/sun/mail/util/logging/DurationFilter.java b/app/src/main/java/com/sun/mail/util/logging/DurationFilter.java new file mode 100644 index 0000000000..b85101468c --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/logging/DurationFilter.java @@ -0,0 +1,444 @@ +/* + * Copyright (c) 2015, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2018 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import static com.sun.mail.util.logging.LogManagerProperties.fromLogManager; +import java.util.logging.*; + +/** + * A filter used to limit log records based on a maximum generation rate. + * + * The duration specified is used to compute the record rate and the amount of + * time the filter will reject records once the rate has been exceeded. Once the + * rate is exceeded records are not allowed until the duration has elapsed. + * + *

+ * By default each {@code DurationFilter} is initialized using the following + * LogManager configuration properties where {@code } refers to the + * fully qualified class name of the handler. If properties are not defined, or + * contain invalid values, then the specified default values are used. + * + *

    + *
  • {@literal }.records the max number of records per duration. + * A numeric long integer or a multiplication expression can be used as the + * value. (defaults to {@code 1000}) + * + *
  • {@literal }.duration the number of milliseconds to suppress + * log records from being published. This is also used as duration to determine + * the log record rate. A numeric long integer or a multiplication expression + * can be used as the value. If the {@code java.time} package is available then + * an ISO-8601 duration format of {@code PnDTnHnMn.nS} can be used as the value. + * The suffixes of "D", "H", "M" and "S" are for days, hours, minutes and + * seconds. The suffixes must occur in order. The seconds can be specified with + * a fractional component to declare milliseconds. (defaults to {@code PT15M}) + *
+ * + *

+ * For example, the settings to limit {@code MailHandler} with a default + * capacity to only send a maximum of two email messages every six minutes would + * be as follows: + *

+ * {@code
+ *  com.sun.mail.util.logging.MailHandler.filter = com.sun.mail.util.logging.DurationFilter
+ *  com.sun.mail.util.logging.MailHandler.capacity = 1000
+ *  com.sun.mail.util.logging.DurationFilter.records = 2L * 1000L
+ *  com.sun.mail.util.logging.DurationFilter.duration = PT6M
+ * }
+ * 
+ * + * + * @author Jason Mehrens + * @since JavaMail 1.5.5 + */ +public class DurationFilter implements Filter { + + /** + * The number of expected records per duration. + */ + private final long records; + /** + * The duration in milliseconds used to determine the rate. The duration is + * also used as the amount of time that the filter will not allow records + * when saturated. + */ + private final long duration; + /** + * The number of records seen for the current duration. This value negative + * if saturated. Zero is considered saturated but is reserved for recording + * the first duration. + */ + private long count; + /** + * The most recent record time seen for the current duration. + */ + private long peak; + /** + * The start time for the current duration. + */ + private long start; + + /** + * Creates the filter using the default properties. + */ + public DurationFilter() { + this.records = checkRecords(initLong(".records")); + this.duration = checkDuration(initLong(".duration")); + } + + /** + * Creates the filter using the given properties. Default values are used if + * any of the given values are outside the allowed range. + * + * @param records the number of records per duration. + * @param duration the number of milliseconds to suppress log records from + * being published. + */ + public DurationFilter(final long records, final long duration) { + this.records = checkRecords(records); + this.duration = checkDuration(duration); + } + + /** + * Determines if this filter is equal to another filter. + * + * @param obj the given object. + * @return true if equal otherwise false. + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { //Avoid locks and deal with rapid state changes. + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + final DurationFilter other = (DurationFilter) obj; + if (this.records != other.records) { + return false; + } + + if (this.duration != other.duration) { + return false; + } + + final long c; + final long p; + final long s; + synchronized (this) { + c = this.count; + p = this.peak; + s = this.start; + } + + synchronized (other) { + if (c != other.count || p != other.peak || s != other.start) { + return false; + } + } + return true; + } + + /** + * Determines if this filter is able to accept the maximum number of log + * records for this instant in time. The result is a best-effort estimate + * and should be considered out of date as soon as it is produced. This + * method is designed for use in monitoring the state of this filter. + * + * @return true if the filter is idle; false otherwise. + */ + public boolean isIdle() { + return test(0L, System.currentTimeMillis()); + } + + /** + * Returns a hash code value for this filter. + * + * @return hash code for this filter. + */ + @Override + public int hashCode() { + int hash = 3; + hash = 89 * hash + (int) (this.records ^ (this.records >>> 32)); + hash = 89 * hash + (int) (this.duration ^ (this.duration >>> 32)); + return hash; + } + + /** + * Check if the given log record should be published. This method will + * modify the internal state of this filter. + * + * @param record the log record to check. + * @return true if allowed; false otherwise. + * @throws NullPointerException if given record is null. + */ + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(final LogRecord record) { + return accept(record.getMillis()); + } + + /** + * Determines if this filter will accept log records for this instant in + * time. The result is a best-effort estimate and should be considered out + * of date as soon as it is produced. This method is designed for use in + * monitoring the state of this filter. + * + * @return true if the filter is not saturated; false otherwise. + */ + public boolean isLoggable() { + return test(records, System.currentTimeMillis()); + } + + /** + * Returns a string representation of this filter. The result is a + * best-effort estimate and should be considered out of date as soon as it + * is produced. + * + * @return a string representation of this filter. + */ + @Override + public String toString() { + boolean idle; + boolean loggable; + synchronized (this) { + final long millis = System.currentTimeMillis(); + idle = test(0L, millis); + loggable = test(records, millis); + } + + return getClass().getName() + "{records=" + records + + ", duration=" + duration + + ", idle=" + idle + + ", loggable=" + loggable + '}'; + } + + /** + * Creates a copy of this filter that retains the filter settings but does + * not include the current filter state. The newly create clone acts as if + * it has never seen any records. + * + * @return a copy of this filter. + * @throws CloneNotSupportedException if this filter is not allowed to be + * cloned. + */ + @Override + protected DurationFilter clone() throws CloneNotSupportedException { + final DurationFilter clone = (DurationFilter) super.clone(); + clone.count = 0L; //Reset the filter state. + clone.peak = 0L; + clone.start = 0L; + return clone; + } + + /** + * Checks if this filter is not saturated or bellow a maximum rate. + * + * @param limit the number of records allowed to be under the rate. + * @param millis the current time in milliseconds. + * @return true if not saturated or bellow the rate. + */ + private boolean test(final long limit, final long millis) { + assert limit >= 0L : limit; + final long c; + final long s; + synchronized (this) { + c = count; + s = start; + } + + if (c > 0L) { //If not saturated. + if ((millis - s) >= duration || c < limit) { + return true; + } + } else { //Subtraction is used to deal with numeric overflow. + if ((millis - s) >= 0L || c == 0L) { + return true; + } + } + return false; + } + + /** + * Determines if the record is loggable by time. + * + * @param millis the log record milliseconds. + * @return true if accepted false otherwise. + */ + private synchronized boolean accept(final long millis) { + //Subtraction is used to deal with numeric overflow of millis. + boolean allow; + if (count > 0L) { //If not saturated. + if ((millis - peak) > 0L) { + peak = millis; //Record the new peak. + } + + //Under the rate if the count has not been reached. + if (count != records) { + ++count; + allow = true; + } else { + if ((peak - start) >= duration) { + count = 1L; //Start a new duration. + start = peak; + allow = true; + } else { + count = -1L; //Saturate for the duration. + start = peak + duration; + allow = false; + } + } + } else { + //If the saturation period has expired or this is the first record + //then start a new duration and allow records. + if ((millis - start) >= 0L || count == 0L) { + count = 1L; + start = millis; + peak = millis; + allow = true; + } else { + allow = false; //Remain in a saturated state. + } + } + return allow; + } + + /** + * Reads a long value or multiplication expression from the LogManager. If + * the value can not be parsed or is not defined then Long.MIN_VALUE is + * returned. + * + * @param suffix a dot character followed by the key name. + * @return a long value or Long.MIN_VALUE if unable to parse or undefined. + * @throws NullPointerException if suffix is null. + */ + private long initLong(final String suffix) { + long result = 0L; + final String p = getClass().getName(); + String value = fromLogManager(p.concat(suffix)); + if (value != null && value.length() != 0) { + value = value.trim(); + if (isTimeEntry(suffix, value)) { + try { + result = LogManagerProperties.parseDurationToMillis(value); + } catch (final RuntimeException ignore) { + } catch (final Exception ignore) { + } catch (final LinkageError ignore) { + } + } + + if (result == 0L) { //Zero is invalid. + try { + result = 1L; + for (String s : tokenizeLongs(value)) { + if (s.endsWith("L") || s.endsWith("l")) { + s = s.substring(0, s.length() - 1); + } + result = multiplyExact(result, Long.parseLong(s)); + } + } catch (final RuntimeException ignore) { + result = Long.MIN_VALUE; + } + } + } else { + result = Long.MIN_VALUE; + } + return result; + } + + /** + * Determines if the given suffix can be a time unit and the value is + * encoded as an ISO ISO-8601 duration format. + * + * @param suffix the suffix property. + * @param value the value of the property. + * @return true if the entry is a time entry. + * @throws IndexOutOfBoundsException if value is empty. + * @throws NullPointerException if either argument is null. + */ + private boolean isTimeEntry(final String suffix, final String value) { + return (value.charAt(0) == 'P' || value.charAt(0) == 'p') + && suffix.equals(".duration"); + } + + /** + * Parse any long value or multiplication expressions into tokens. + * + * @param value the expression or value. + * @return an array of long tokens, never empty. + * @throws NullPointerException if the given value is null. + * @throws NumberFormatException if the expression is invalid. + */ + private static String[] tokenizeLongs(final String value) { + String[] e; + final int i = value.indexOf('*'); + if (i > -1 && (e = value.split("\\s*\\*\\s*")).length != 0) { + if (i == 0 || value.charAt(value.length() - 1) == '*') { + throw new NumberFormatException(value); + } + + if (e.length == 1) { + throw new NumberFormatException(e[0]); + } + } else { + e = new String[]{value}; + } + return e; + } + + /** + * Multiply and check for overflow. This can be replaced with + * {@code java.lang.Math.multiplyExact} when Jakarta Mail requires JDK 8. + * + * @param x the first value. + * @param y the second value. + * @return x times y. + * @throws ArithmeticException if overflow is detected. + */ + private static long multiplyExact(final long x, final long y) { + long r = x * y; + if (((Math.abs(x) | Math.abs(y)) >>> 31L != 0L)) { + if (((y != 0L) && (r / y != x)) + || (x == Long.MIN_VALUE && y == -1L)) { + throw new ArithmeticException(); + } + } + return r; + } + + /** + * Converts record count to a valid record count. If the value is out of + * bounds then the default record count is used. + * + * @param records the record count. + * @return a valid number of record count. + */ + private static long checkRecords(final long records) { + return records > 0L ? records : 1000L; + } + + /** + * Converts the duration to a valid duration. If the value is out of bounds + * then the default duration is used. + * + * @param duration the duration to check. + * @return a valid duration. + */ + private static long checkDuration(final long duration) { + return duration > 0L ? duration : 15L * 60L * 1000L; + } +} diff --git a/app/src/main/java/com/sun/mail/util/logging/LogManagerProperties.java b/app/src/main/java/com/sun/mail/util/logging/LogManagerProperties.java new file mode 100644 index 0000000000..e2ec6c575e --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/logging/LogManagerProperties.java @@ -0,0 +1,1090 @@ +/* + * Copyright (c) 2009, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2019 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.UndeclaredThrowableException; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.*; +import java.util.logging.*; +import java.util.logging.Formatter; + +/** + * An adapter class to allow the Mail API to access the LogManager properties. + * The LogManager properties are treated as the root of all properties. First, + * the parent properties are searched. If no value is found, then, the + * LogManager is searched with prefix value. If not found, then, just the key + * itself is searched in the LogManager. If a value is found in the LogManager + * it is then copied to this properties object with no key prefix. If no value + * is found in the LogManager or the parent properties, then this properties + * object is searched only by passing the key value. + * + *

+ * This class also emulates the LogManager functions for creating new objects + * from string class names. This is to support initial setup of objects such as + * log filters, formatters, error managers, etc. + * + *

+ * This class should never be exposed outside of this package. Keep this class + * package private (default access). + * + * @author Jason Mehrens + * @since JavaMail 1.4.3 + */ +final class LogManagerProperties extends Properties { + + /** + * Generated serial id. + */ + private static final long serialVersionUID = -2239983349056806252L; + /** + * Holds the method used to get the LogRecord instant if running on JDK 9 or + * later. + */ + private static final Method LR_GET_INSTANT; + + /** + * Holds the method used to get the default time zone if running on JDK 9 or + * later. + */ + private static final Method ZI_SYSTEM_DEFAULT; + + /** + * Holds the method used to convert and instant to a zoned date time if + * running on JDK 9 later. + */ + private static final Method ZDT_OF_INSTANT; + + static { + Method lrgi = null; + Method zisd = null; + Method zdtoi = null; + try { + lrgi = LogRecord.class.getMethod("getInstant"); + assert Comparable.class + .isAssignableFrom(lrgi.getReturnType()) : lrgi; + zisd = findClass("java.time.ZoneId") + .getMethod("systemDefault"); + if (!Modifier.isStatic(zisd.getModifiers())) { + throw new NoSuchMethodException(zisd.toString()); + } + + zdtoi = findClass("java.time.ZonedDateTime") + .getMethod("ofInstant", findClass("java.time.Instant"), + findClass("java.time.ZoneId")); + if (!Modifier.isStatic(zdtoi.getModifiers())) { + throw new NoSuchMethodException(zdtoi.toString()); + } + + if (!Comparable.class.isAssignableFrom(zdtoi.getReturnType())) { + throw new NoSuchMethodException(zdtoi.toString()); + } + } catch (final RuntimeException ignore) { + } catch (final Exception ignore) { //No need for specific catch. + } catch (final LinkageError ignore) { + } finally { + if (lrgi == null || zisd == null || zdtoi == null) { + lrgi = null; //If any are null then clear all. + zisd = null; + zdtoi = null; + } + } + + LR_GET_INSTANT = lrgi; + ZI_SYSTEM_DEFAULT = zisd; + ZDT_OF_INSTANT = zdtoi; + } + /** + * Caches the read only reflection class names string array. Declared + * volatile for safe publishing only. The VO_VOLATILE_REFERENCE_TO_ARRAY + * warning is a false positive. + */ + @SuppressWarnings("VolatileArrayField") + private static volatile String[] REFLECT_NAMES; + /** + * Caches the LogManager or Properties so we only read the configuration + * once. + */ + private static final Object LOG_MANAGER = loadLogManager(); + + /** + * Get the LogManager or loads a Properties object to use as the LogManager. + * + * @return the LogManager or a loaded Properties object. + * @since JavaMail 1.5.3 + */ + private static Object loadLogManager() { + Object m; + try { + m = LogManager.getLogManager(); + } catch (final LinkageError restricted) { + m = readConfiguration(); + } catch (final RuntimeException unexpected) { + m = readConfiguration(); + } + return m; + } + + /** + * Create a properties object from the default logging configuration file. + * Since the LogManager is not available in restricted environments, only + * the default configuration is applicable. + * + * @return a properties object loaded with the default configuration. + * @since JavaMail 1.5.3 + */ + private static Properties readConfiguration() { + /** + * Load the properties file so the default settings are available when + * user code creates a logging object. The class loader for the + * restricted LogManager can't access these classes to attach them to a + * logger or handler on startup. Creating logging objects at this point + * is both useless and risky. + */ + final Properties props = new Properties(); + try { + String n = System.getProperty("java.util.logging.config.file"); + if (n != null) { + final File f = new File(n).getCanonicalFile(); + final InputStream in = new FileInputStream(f); + try { + props.load(in); + } finally { + in.close(); + } + } + } catch (final RuntimeException permissionsOrMalformed) { + } catch (final Exception ioe) { + } catch (final LinkageError unexpected) { + } + return props; + } + + /** + * Gets LogManger property for the running JVM. If the LogManager doesn't + * exist then the default LogManger properties are used. + * + * @param name the property name. + * @return the LogManager. + * @throws NullPointerException if the given name is null. + * @since JavaMail 1.5.3 + */ + static String fromLogManager(final String name) { + if (name == null) { + throw new NullPointerException(); + } + + final Object m = LOG_MANAGER; + try { + if (m instanceof Properties) { + return ((Properties) m).getProperty(name); + } + } catch (final RuntimeException unexpected) { + } + + if (m != null) { + try { + if (m instanceof LogManager) { + return ((LogManager) m).getProperty(name); + } + } catch (final LinkageError restricted) { + } catch (final RuntimeException unexpected) { + } + } + return null; + } + + /** + * Check that the current context is trusted to modify the logging + * configuration. This requires LoggingPermission("control"). + * @throws SecurityException if a security manager exists and the caller + * does not have {@code LoggingPermission("control")}. + * @since JavaMail 1.5.3 + */ + static void checkLogManagerAccess() { + boolean checked = false; + final Object m = LOG_MANAGER; + if (m != null) { + try { + if (m instanceof LogManager) { + checked = true; + ((LogManager) m).checkAccess(); + } + } catch (final SecurityException notAllowed) { + if (checked) { + throw notAllowed; + } + } catch (final LinkageError restricted) { + } catch (final RuntimeException unexpected) { + } + } + + if (!checked) { + checkLoggingAccess(); + } + } + + /** + * Check that the current context is trusted to modify the logging + * configuration when the LogManager is not present. This requires + * LoggingPermission("control"). + * @throws SecurityException if a security manager exists and the caller + * does not have {@code LoggingPermission("control")}. + * @since JavaMail 1.5.3 + */ + private static void checkLoggingAccess() { + /** + * Some environments selectively enforce logging permissions by allowing + * access to loggers but not allowing access to handlers. This is an + * indirect way of checking for LoggingPermission when the LogManager is + * not present. The root logger will lazy create handlers so the global + * logger is used instead as it is a known named logger with well + * defined behavior. If the global logger is a subclass then fallback to + * using the SecurityManager. + */ + boolean checked = false; + final Logger global = Logger.getLogger("global"); + try { + if (Logger.class == global.getClass()) { + global.removeHandler((Handler) null); + checked = true; + } + } catch (final NullPointerException unexpected) { + } + + if (!checked) { + final SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + sm.checkPermission(new LoggingPermission("control", null)); + } + } + } + + /** + * Determines if access to the {@code java.util.logging.LogManager} class is + * restricted by the class loader. + * + * @return true if a LogManager is present. + * @since JavaMail 1.5.3 + */ + static boolean hasLogManager() { + final Object m = LOG_MANAGER; + return m != null && !(m instanceof Properties); + } + + /** + * Gets the ZonedDateTime from the given log record. + * + * @param record used to generate the zoned date time. + * @return null if LogRecord doesn't support nanoseconds otherwise a new + * zoned date time is returned. + * @throws NullPointerException if record is null. + * @since JavaMail 1.5.6 + */ + @SuppressWarnings("UseSpecificCatch") + static Comparable getZonedDateTime(LogRecord record) { + if (record == null) { + throw new NullPointerException(); + } + final Method m = ZDT_OF_INSTANT; + if (m != null) { + try { + return (Comparable) m.invoke((Object) null, + LR_GET_INSTANT.invoke(record), + ZI_SYSTEM_DEFAULT.invoke((Object) null)); + } catch (final RuntimeException ignore) { + assert LR_GET_INSTANT != null + && ZI_SYSTEM_DEFAULT != null : ignore; + } catch (final InvocationTargetException ite) { + final Throwable cause = ite.getCause(); + if (cause instanceof Error) { + throw (Error) cause; + } else if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else { //Should never happen. + throw new UndeclaredThrowableException(ite); + } + } catch (final Exception ignore) { + } + } + return null; + } + + /** + * Gets the local host name from the given service. + * + * @param s the service to examine. + * @return the local host name or null. + * @throws IllegalAccessException if the method is inaccessible. + * @throws InvocationTargetException if the method throws an exception. + * @throws LinkageError if the linkage fails. + * @throws NullPointerException if the given service is null. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception if there is a problem. + * @throws NoSuchMethodException if the given service does not have a method + * to get the local host name as a string. + * @throws SecurityException if unable to inspect properties of object. + * @since JavaMail 1.5.3 + */ + static String getLocalHost(final Object s) throws Exception { + try { + final Method m = s.getClass().getMethod("getLocalHost"); + if (!Modifier.isStatic(m.getModifiers()) + && m.getReturnType() == String.class) { + return (String) m.invoke(s); + } else { + throw new NoSuchMethodException(m.toString()); + } + } catch (final ExceptionInInitializerError EIIE) { + throw wrapOrThrow(EIIE); + } catch (final InvocationTargetException ite) { + throw paramOrError(ite); + } + } + + /** + * Used to parse an ISO-8601 duration format of {@code PnDTnHnMn.nS}. + * + * @param value an ISO-8601 duration character sequence. + * @return the number of milliseconds parsed from the duration. + * @throws ClassNotFoundException if the java.time classes are not present. + * @throws IllegalAccessException if the method is inaccessible. + * @throws InvocationTargetException if the method throws an exception. + * @throws LinkageError if the linkage fails. + * @throws NullPointerException if the given duration is null. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception if there is a problem. + * @throws NoSuchMethodException if the correct time methods are missing. + * @throws SecurityException if reflective access to the java.time classes + * are not allowed. + * @since JavaMail 1.5.5 + */ + static long parseDurationToMillis(final CharSequence value) throws Exception { + try { + final Class k = findClass("java.time.Duration"); + final Method parse = k.getMethod("parse", CharSequence.class); + if (!k.isAssignableFrom(parse.getReturnType()) + || !Modifier.isStatic(parse.getModifiers())) { + throw new NoSuchMethodException(parse.toString()); + } + + final Method toMillis = k.getMethod("toMillis"); + if (!Long.TYPE.isAssignableFrom(toMillis.getReturnType()) + || Modifier.isStatic(toMillis.getModifiers())) { + throw new NoSuchMethodException(toMillis.toString()); + } + return (Long) toMillis.invoke(parse.invoke(null, value)); + } catch (final ExceptionInInitializerError EIIE) { + throw wrapOrThrow(EIIE); + } catch (final InvocationTargetException ite) { + throw paramOrError(ite); + } + } + + /** + * Converts a locale to a language tag. + * + * @param locale the locale to convert. + * @return the language tag. + * @throws NullPointerException if the given locale is null. + * @since JavaMail 1.4.5 + */ + static String toLanguageTag(final Locale locale) { + final String l = locale.getLanguage(); + final String c = locale.getCountry(); + final String v = locale.getVariant(); + final char[] b = new char[l.length() + c.length() + v.length() + 2]; + int count = l.length(); + l.getChars(0, count, b, 0); + if (c.length() != 0 || (l.length() != 0 && v.length() != 0)) { + b[count] = '-'; + ++count; //be nice to the client compiler. + c.getChars(0, c.length(), b, count); + count += c.length(); + } + + if (v.length() != 0 && (l.length() != 0 || c.length() != 0)) { + b[count] = '-'; + ++count; //be nice to the client compiler. + v.getChars(0, v.length(), b, count); + count += v.length(); + } + return String.valueOf(b, 0, count); + } + + /** + * Creates a new filter from the given class name. + * + * @param name the fully qualified class name. + * @return a new filter. + * @throws ClassCastException if class name does not match the type. + * @throws ClassNotFoundException if the class name was not found. + * @throws IllegalAccessException if the constructor is inaccessible. + * @throws InstantiationException if the given class name is abstract. + * @throws InvocationTargetException if the constructor throws an exception. + * @throws LinkageError if the linkage fails. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception to match the error method of the ErrorManager. + * @throws NoSuchMethodException if the class name does not have a no + * argument constructor. + * @since JavaMail 1.4.5 + */ + static Filter newFilter(String name) throws Exception { + return newObjectFrom(name, Filter.class); + } + + /** + * Creates a new formatter from the given class name. + * + * @param name the fully qualified class name. + * @return a new formatter. + * @throws ClassCastException if class name does not match the type. + * @throws ClassNotFoundException if the class name was not found. + * @throws IllegalAccessException if the constructor is inaccessible. + * @throws InstantiationException if the given class name is abstract. + * @throws InvocationTargetException if the constructor throws an exception. + * @throws LinkageError if the linkage fails. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception to match the error method of the ErrorManager. + * @throws NoSuchMethodException if the class name does not have a no + * argument constructor. + * @since JavaMail 1.4.5 + */ + static Formatter newFormatter(String name) throws Exception { + return newObjectFrom(name, Formatter.class); + } + + /** + * Creates a new log record comparator from the given class name. + * + * @param name the fully qualified class name. + * @return a new comparator. + * @throws ClassCastException if class name does not match the type. + * @throws ClassNotFoundException if the class name was not found. + * @throws IllegalAccessException if the constructor is inaccessible. + * @throws InstantiationException if the given class name is abstract. + * @throws InvocationTargetException if the constructor throws an exception. + * @throws LinkageError if the linkage fails. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception to match the error method of the ErrorManager. + * @throws NoSuchMethodException if the class name does not have a no + * argument constructor. + * @since JavaMail 1.4.5 + * @see java.util.logging.LogRecord + */ + @SuppressWarnings("unchecked") + static Comparator newComparator(String name) throws Exception { + return newObjectFrom(name, Comparator.class); + } + + /** + * Returns a comparator that imposes the reverse ordering of the specified + * {@link Comparator}. If the given comparator declares a public + * reverseOrder method that method is called first and the return value is + * used. If that method is not declared or the caller does not have access + * then a comparator wrapping the given comparator is returned. + * + * @param the element type to be compared + * @param c a comparator whose ordering is to be reversed by the returned + * comparator + * @return A comparator that imposes the reverse ordering of the specified + * comparator. + * @throws NullPointerException if the given comparator is null. + * @since JavaMail 1.5.0 + */ + @SuppressWarnings({"unchecked", "ThrowableResultIgnored"}) + static Comparator reverseOrder(final Comparator c) { + if (c == null) { + throw new NullPointerException(); + } + + Comparator reverse = null; + //Comparator in Java 1.8 has 'reversed' as a default method. + //This code calls that method first to allow custom + //code to define what reverse order means. + try { + //assert Modifier.isPublic(c.getClass().getModifiers()) : + // Modifier.toString(c.getClass().getModifiers()); + final Method m = c.getClass().getMethod("reversed"); + if (!Modifier.isStatic(m.getModifiers()) + && Comparator.class.isAssignableFrom(m.getReturnType())) { + try { + reverse = (Comparator) m.invoke(c); + } catch (final ExceptionInInitializerError eiie) { + throw wrapOrThrow(eiie); + } + } + } catch (final NoSuchMethodException ignore) { + } catch (final IllegalAccessException ignore) { + } catch (final RuntimeException ignore) { + } catch (final InvocationTargetException ite) { + paramOrError(ite); //Ignore invocation bugs (returned values). + } + + if (reverse == null) { + reverse = Collections.reverseOrder(c); + } + return reverse; + } + + /** + * Creates a new error manager from the given class name. + * + * @param name the fully qualified class name. + * @return a new error manager. + * @throws ClassCastException if class name does not match the type. + * @throws ClassNotFoundException if the class name was not found. + * @throws IllegalAccessException if the constructor is inaccessible. + * @throws InstantiationException if the given class name is abstract. + * @throws InvocationTargetException if the constructor throws an exception. + * @throws LinkageError if the linkage fails. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception to match the error method of the ErrorManager. + * @throws NoSuchMethodException if the class name does not have a no + * argument constructor. + * @since JavaMail 1.4.5 + */ + static ErrorManager newErrorManager(String name) throws Exception { + return newObjectFrom(name, ErrorManager.class); + } + + /** + * Determines if the given class name identifies a utility class. + * + * @param name the fully qualified class name. + * @return true if the given class name + * @throws ClassNotFoundException if the class name was not found. + * @throws IllegalAccessException if the constructor is inaccessible. + * @throws LinkageError if the linkage fails. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception to match the error method of the ErrorManager. + * @throws SecurityException if unable to inspect properties of class. + * @since JavaMail 1.5.2 + */ + static boolean isStaticUtilityClass(String name) throws Exception { + final Class c = findClass(name); + final Class obj = Object.class; + Method[] methods; + boolean util; + if (c != obj && (methods = c.getMethods()).length != 0) { + util = true; + for (Method m : methods) { + if (m.getDeclaringClass() != obj + && !Modifier.isStatic(m.getModifiers())) { + util = false; + break; + } + } + } else { + util = false; + } + return util; + } + + /** + * Determines if the given class name is a reflection class name responsible + * for invoking methods and or constructors. + * + * @param name the fully qualified class name. + * @return true if the given class name + * @throws ClassNotFoundException if the class name was not found. + * @throws IllegalAccessException if the constructor is inaccessible. + * @throws LinkageError if the linkage fails. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception to match the error method of the ErrorManager. + * @throws SecurityException if unable to inspect properties of class. + * @since JavaMail 1.5.2 + */ + static boolean isReflectionClass(String name) throws Exception { + String[] names = REFLECT_NAMES; + if (names == null) { //Benign data race. + REFLECT_NAMES = names = reflectionClassNames(); + } + + for (String rf : names) { //The set of names is small. + if (name.equals(rf)) { + return true; + } + } + + findClass(name); //Fail late instead of normal return. + return false; + } + + /** + * Determines all of the reflection class names used to invoke methods. + * + * This method performs indirect and direct calls on a throwable to capture + * the standard class names and the implementation class names. + * + * @return a string array containing the fully qualified class names. + * @throws Exception if there is a problem. + */ + private static String[] reflectionClassNames() throws Exception { + final Class thisClass = LogManagerProperties.class; + assert Modifier.isFinal(thisClass.getModifiers()) : thisClass; + try { + final HashSet traces = new HashSet<>(); + Throwable t = Throwable.class.getConstructor().newInstance(); + for (StackTraceElement ste : t.getStackTrace()) { + if (!thisClass.getName().equals(ste.getClassName())) { + traces.add(ste.getClassName()); + } else { + break; + } + } + + Throwable.class.getMethod("fillInStackTrace").invoke(t); + for (StackTraceElement ste : t.getStackTrace()) { + if (!thisClass.getName().equals(ste.getClassName())) { + traces.add(ste.getClassName()); + } else { + break; + } + } + return traces.toArray(new String[traces.size()]); + } catch (final InvocationTargetException ITE) { + throw paramOrError(ITE); + } + } + + /** + * Creates a new object from the given class name. + * + * @param The generic class type. + * @param name the fully qualified class name. + * @param type the assignable type for the given name. + * @return a new object assignable to the given type. + * @throws ClassCastException if class name does not match the type. + * @throws ClassNotFoundException if the class name was not found. + * @throws IllegalAccessException if the constructor is inaccessible. + * @throws InstantiationException if the given class name is abstract. + * @throws InvocationTargetException if the constructor throws an exception. + * @throws LinkageError if the linkage fails. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception to match the error method of the ErrorManager. + * @throws NoSuchMethodException if the class name does not have a no + * argument constructor. + * @since JavaMail 1.4.5 + */ + static T newObjectFrom(String name, Class type) throws Exception { + try { + final Class clazz = LogManagerProperties.findClass(name); + //This check avoids additional side effects when the name parameter + //is a literal name and not a class name. + if (type.isAssignableFrom(clazz)) { + try { + return type.cast(clazz.getConstructor().newInstance()); + } catch (final InvocationTargetException ITE) { + throw paramOrError(ITE); + } + } else { + throw new ClassCastException(clazz.getName() + + " cannot be cast to " + type.getName()); + } + } catch (final NoClassDefFoundError NCDFE) { + //No class def found can occur on filesystems that are + //case insensitive (BUG ID 6196068). In some cases, we allow class + //names or literal names, this code guards against the case where a + //literal name happens to match a class name in a different case. + //This is also a nice way to adapt this error for the error manager. + throw new ClassNotFoundException(NCDFE.toString(), NCDFE); + } catch (final ExceptionInInitializerError EIIE) { + throw wrapOrThrow(EIIE); + } + } + + /** + * Returns the given exception or throws the escaping cause. + * + * @param ite any invocation target. + * @return the exception. + * @throws VirtualMachineError if present as cause. + * @throws ThreadDeath if present as cause. + * @since JavaMail 1.4.5 + */ + private static Exception paramOrError(InvocationTargetException ite) { + final Throwable cause = ite.getCause(); + if (cause != null) { + //Bitwise inclusive OR produces tighter bytecode for instanceof + //and matches with multicatch syntax. + if (cause instanceof VirtualMachineError + | cause instanceof ThreadDeath) { + throw (Error) cause; + } + } + return ite; + } + + /** + * Throws the given error if the cause is an error otherwise the given error + * is wrapped. + * + * @param eiie the error. + * @return an InvocationTargetException. + * @since JavaMail 1.5.0 + */ + private static InvocationTargetException wrapOrThrow( + ExceptionInInitializerError eiie) { + //This linkage error will escape the constructor new instance call. + //If the cause is an error, rethrow to skip any error manager. + if (eiie.getCause() instanceof Error) { + throw eiie; + } else { + //Considered a bug in the code, wrap the error so it can be + //reported to the error manager. + return new InvocationTargetException(eiie); + } + } + + /** + * This code is modified from the LogManager, which explictly states + * searching the system class loader first, then the context class loader. + * There is resistance (compatibility) to change this behavior to simply + * searching the context class loader. + * + * @param name full class name + * @return the class. + * @throws LinkageError if the linkage fails. + * @throws ClassNotFoundException if the class name was not found. + * @throws ExceptionInInitializerError if static initializer fails. + */ + private static Class findClass(String name) throws ClassNotFoundException { + ClassLoader[] loaders = getClassLoaders(); + assert loaders.length == 2 : loaders.length; + Class clazz; + if (loaders[0] != null) { + try { + clazz = Class.forName(name, false, loaders[0]); + } catch (ClassNotFoundException tryContext) { + clazz = tryLoad(name, loaders[1]); + } + } else { + clazz = tryLoad(name, loaders[1]); + } + return clazz; + } + + /** + * Loads a class using the given loader or the class loader of this class. + * + * @param name the class name. + * @param l any class loader or null. + * @return the raw class. + * @throws ClassNotFoundException if not found. + */ + private static Class tryLoad(String name, ClassLoader l) throws ClassNotFoundException { + if (l != null) { + return Class.forName(name, false, l); + } else { + return Class.forName(name); + } + } + + /** + * Gets the class loaders using elevated privileges. + * + * @return any array of class loaders. Indexes may be null. + */ + private static ClassLoader[] getClassLoaders() { + return AccessController.doPrivileged(new PrivilegedAction() { + + @SuppressWarnings("override") //JDK-6954234 + public ClassLoader[] run() { + final ClassLoader[] loaders = new ClassLoader[2]; + try { + loaders[0] = ClassLoader.getSystemClassLoader(); + } catch (SecurityException ignore) { + loaders[0] = null; + } + + try { + loaders[1] = Thread.currentThread().getContextClassLoader(); + } catch (SecurityException ignore) { + loaders[1] = null; + } + return loaders; + } + }); + } + /** + * The namespace prefix to search LogManager and defaults. + */ + private final String prefix; + + /** + * Creates a log manager properties object. + * + * @param parent the parent properties. + * @param prefix the namespace prefix. + * @throws NullPointerException if prefix or + * parent is null. + */ + LogManagerProperties(final Properties parent, final String prefix) { + super(parent); + if (parent == null || prefix == null) { + throw new NullPointerException(); + } + this.prefix = prefix; + } + + /** + * Returns a properties object that contains a snapshot of the current + * state. This method violates the clone contract so that no instances of + * LogManagerProperties is exported for public use. + * + * @return the snapshot. + * @since JavaMail 1.4.4 + */ + @Override + @SuppressWarnings("CloneDoesntCallSuperClone") + public synchronized Object clone() { + return exportCopy(defaults); + } + + /** + * Searches defaults, then searches the log manager if available or the + * system properties by the prefix property, and then by the key itself. + * + * @param key a non null key. + * @return the value for that key. + */ + @Override + public synchronized String getProperty(final String key) { + String value = defaults.getProperty(key); + if (value == null) { + if (key.length() > 0) { + value = fromLogManager(prefix + '.' + key); + } + + if (value == null) { + value = fromLogManager(key); + } + + /** + * Copy the log manager properties as we read them. If a value is no + * longer present in the LogManager read it from here. The reason + * this works is because LogManager.reset() closes all attached + * handlers therefore, stale values only exist in closed handlers. + */ + if (value != null) { + super.put(key, value); + } else { + Object v = super.get(key); //defaults are not used. + value = v instanceof String ? (String) v : null; + } + } + return value; + } + + /** + * Calls getProperty directly. If getProperty returns null the default value + * is returned. + * + * @param key a key to search for. + * @param def the default value to use if not found. + * @return the value for the key. + * @since JavaMail 1.4.4 + */ + @Override + public String getProperty(final String key, final String def) { + final String value = this.getProperty(key); + return value == null ? def : value; + } + + /** + * Required to work with PropUtil. Calls getProperty directly if the given + * key is a string. Otherwise, performs a get operation on the defaults + * followed by the normal hash table get. + * + * @param key any key. + * @return the value for the key or null. + * @since JavaMail 1.4.5 + */ + @Override + public synchronized Object get(final Object key) { + Object value; + if (key instanceof String) { + value = getProperty((String) key); + } else { + value = null; + } + + //Search for non-string value. + if (value == null) { + value = defaults.get(key); + if (value == null && !defaults.containsKey(key)) { + value = super.get(key); + } + } + return value; + } + + /** + * Required to work with PropUtil. An updated copy of the key is fetched + * from the log manager if the key doesn't exist in this properties. + * + * @param key any key. + * @return the value for the key or the default value for the key. + * @since JavaMail 1.4.5 + */ + @Override + public synchronized Object put(final Object key, final Object value) { + if (key instanceof String && value instanceof String) { + final Object def = preWrite(key); + final Object man = super.put(key, value); + return man == null ? def : man; + } else { + return super.put(key, value); + } + } + + /** + * Calls the put method directly. + * + * @param key any key. + * @return the value for the key or the default value for the key. + * @since JavaMail 1.4.5 + */ + @Override + public Object setProperty(String key, String value) { + return this.put(key, value); + } + + /** + * Required to work with PropUtil. An updated copy of the key is fetched + * from the log manager prior to returning. + * + * @param key any key. + * @return the value for the key or null. + * @since JavaMail 1.4.5 + */ + @Override + public synchronized boolean containsKey(final Object key) { + boolean found = key instanceof String + && getProperty((String) key) != null; + if (!found) { + found = defaults.containsKey(key) || super.containsKey(key); + } + return found; + } + + /** + * Required to work with PropUtil. An updated copy of the key is fetched + * from the log manager if the key doesn't exist in this properties. + * + * @param key any key. + * @return the value for the key or the default value for the key. + * @since JavaMail 1.4.5 + */ + @Override + public synchronized Object remove(final Object key) { + final Object def = preWrite(key); + final Object man = super.remove(key); + return man == null ? def : man; + } + + /** + * It is assumed that this method will never be called. No way to get the + * property names from LogManager. + * + * @return the property names + */ + @Override + public Enumeration propertyNames() { + assert false; + return super.propertyNames(); + } + + /** + * It is assumed that this method will never be called. The prefix value is + * not used for the equals method. + * + * @param o any object or null. + * @return true if equal, otherwise false. + */ + @Override + public boolean equals(final Object o) { + if (o == null) { + return false; + } + if (o == this) { + return true; + } + if (o instanceof Properties == false) { + return false; + } + assert false : prefix; + return super.equals(o); + } + + /** + * It is assumed that this method will never be called. See equals. + * + * @return the hash code. + */ + @Override + public int hashCode() { + assert false : prefix.hashCode(); + return super.hashCode(); + } + + /** + * Called before a write operation of a key. Caches a key read from the log + * manager in this properties object. The key is only cached if it is an + * instance of a String and this properties doesn't contain a copy of the + * key. + * + * @param key the key to search. + * @return the default value for the key. + */ + private Object preWrite(final Object key) { + assert Thread.holdsLock(this); + return get(key); + } + + /** + * Creates a public snapshot of this properties object using the given + * parent properties. + * + * @param parent the defaults to use with the snapshot. + * @return the safe snapshot. + */ + private Properties exportCopy(final Properties parent) { + Thread.holdsLock(this); + final Properties child = new Properties(parent); + child.putAll(this); + return child; + } + + /** + * It is assumed that this method will never be called. We return a safe + * copy for export to avoid locking this properties object or the defaults + * during write. + * + * @return the parent properties. + * @throws ObjectStreamException if there is a problem. + */ + private synchronized Object writeReplace() throws ObjectStreamException { + assert false; + return exportCopy((Properties) defaults.clone()); + } +} diff --git a/app/src/main/java/com/sun/mail/util/logging/MailHandler.java b/app/src/main/java/com/sun/mail/util/logging/MailHandler.java new file mode 100644 index 0000000000..ecfe0684a4 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/logging/MailHandler.java @@ -0,0 +1,4416 @@ +/* + * Copyright (c) 2009, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2020 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util.logging; + +import static com.sun.mail.util.logging.LogManagerProperties.fromLogManager; +import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.net.InetAddress; +import java.net.URLConnection; +import java.net.UnknownHostException; +import java.nio.charset.Charset; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.*; +import java.util.logging.*; +import java.util.logging.Formatter; +import javax.activation.*; +import javax.mail.*; +import javax.mail.internet.*; +import javax.mail.util.ByteArrayDataSource; + +/** + * Handler that formats log records as an email message. + * + *

+ * This Handler will store a fixed number of log records used to + * generate a single email message. When the internal buffer reaches capacity, + * all log records are formatted and placed in an email which is sent to an + * email server. The code to manually setup this handler can be as simple as + * the following: + * + *

+ *      Properties props = new Properties();
+ *      props.put("mail.smtp.host", "my-mail-server");
+ *      props.put("mail.to", "me@example.com");
+ *      props.put("verify", "local");
+ *      MailHandler h = new MailHandler(props);
+ *      h.setLevel(Level.WARNING);
+ * 
+ * + *

+ * Configuration: + * The LogManager should define at least one or more recipient addresses and a + * mail host for outgoing email. The code to setup this handler via the + * logging properties can be as simple as the following: + * + *

+ *      #Default MailHandler settings.
+ *      com.sun.mail.util.logging.MailHandler.mail.smtp.host = my-mail-server
+ *      com.sun.mail.util.logging.MailHandler.mail.to = me@example.com
+ *      com.sun.mail.util.logging.MailHandler.level = WARNING
+ *      com.sun.mail.util.logging.MailHandler.verify = local
+ * 
+ * + * For a custom handler, e.g. com.foo.MyHandler, the properties + * would be: + * + *
+ *      #Subclass com.foo.MyHandler settings.
+ *      com.foo.MyHandler.mail.smtp.host = my-mail-server
+ *      com.foo.MyHandler.mail.to = me@example.com
+ *      com.foo.MyHandler.level = WARNING
+ *      com.foo.MyHandler.verify = local
+ * 
+ * + * All mail properties documented in the Java Mail API cascade to + * the LogManager by prefixing a key using the fully qualified class name of + * this MailHandler or the fully qualified derived class name dot + * mail property. If the prefixed property is not found, then the mail property + * itself is searched in the LogManager. By default each + * MailHandler is initialized using the following LogManager + * configuration properties where <handler-name> refers to + * the fully qualified class name of the handler. If properties are not + * defined, or contain invalid values, then the specified default values are + * used. + * + *
    + *
  • <handler-name>.attachment.filters a comma + * separated list of Filter class names used to create each + * attachment. The literal null is reserved for attachments that + * do not require filtering. (defaults to the + * {@linkplain java.util.logging.Handler#getFilter() body} filter) + * + *
  • <handler-name>.attachment.formatters a comma + * separated list of Formatter class names used to create each + * attachment. (default is no attachments) + * + *
  • <handler-name>.attachment.names a comma separated + * list of names or Formatter class names of each attachment. All + * control characters are removed from the attachment names. + * (default is {@linkplain java.util.logging.Formatter#toString() toString} + * of the attachment formatter) + * + *
  • <handler-name>.authenticator name of an + * {@linkplain javax.mail.Authenticator} class used to provide login credentials + * to the email server or string literal that is the password used with the + * {@linkplain Authenticator#getDefaultUserName() default} user name. + * (default is null) + * + *
  • <handler-name>.capacity the max number of + * LogRecord objects include in each email message. + * (defaults to 1000) + * + *
  • <handler-name>.comparator name of a + * {@linkplain java.util.Comparator} class used to sort the published + * LogRecord objects prior to all formatting. + * (defaults to null meaning records are unsorted). + * + *
  • <handler-name>.comparator.reverse a boolean + * true to reverse the order of the specified comparator or + * false to retain the original order. + * (defaults to false) + * + *
  • <handler-name>.encoding the name of the Java + * {@linkplain java.nio.charset.Charset#name() character set} to use for the + * email message. (defaults to null, the + * {@linkplain javax.mail.internet.MimeUtility#getDefaultJavaCharset() default} + * platform encoding). + * + *
  • <handler-name>.errorManager name of an + * ErrorManager class used to handle any configuration or mail + * transport problems. (defaults to java.util.logging.ErrorManager) + * + *
  • <handler-name>.filter name of a Filter + * class used for the body of the message. (defaults to null, + * allow all records) + * + *
  • <handler-name>.formatter name of a + * Formatter class used to format the body of this message. + * (defaults to java.util.logging.SimpleFormatter) + * + *
  • <handler-name>.level specifies the default level + * for this Handler (defaults to Level.WARNING). + * + *
  • <handler-name>.mail.bcc a comma separated list of + * addresses which will be blind carbon copied. Typically, this is set to the + * recipients that may need to be privately notified of a log message or + * notified that a log message was sent to a third party such as a support team. + * The empty string can be used to specify no blind carbon copied address. + * (defaults to null, none) + * + *
  • <handler-name>.mail.cc a comma separated list of + * addresses which will be carbon copied. Typically, this is set to the + * recipients that may need to be notified of a log message but, are not + * required to provide direct support. The empty string can be used to specify + * no carbon copied address. (defaults to null, none) + * + *
  • <handler-name>.mail.from a comma separated list of + * addresses which will be from addresses. Typically, this is set to the email + * address identifying the user running the application. The empty string can + * be used to override the default behavior and specify no from address. + * (defaults to the {@linkplain javax.mail.Message#setFrom() local address}) + * + *
  • <handler-name>.mail.host the host name or IP + * address of the email server. (defaults to null, use + * {@linkplain Transport#protocolConnect default} + * Java Mail behavior) + * + *
  • <handler-name>.mail.reply.to a comma separated + * list of addresses which will be reply-to addresses. Typically, this is set + * to the recipients that provide support for the application itself. The empty + * string can be used to specify no reply-to address. + * (defaults to null, none) + * + *
  • <handler-name>.mail.to a comma separated list of + * addresses which will be send-to addresses. Typically, this is set to the + * recipients that provide support for the application, system, and/or + * supporting infrastructure. The empty string can be used to specify no + * send-to address which overrides the default behavior. (defaults to + * {@linkplain javax.mail.internet.InternetAddress#getLocalAddress + * local address}.) + * + *
  • <handler-name>.mail.sender a single address + * identifying sender of the email; never equal to the from address. Typically, + * this is set to the email address identifying the application itself. The + * empty string can be used to specify no sender address. + * (defaults to null, none) + * + *
  • <handler-name>.subject the name of a + * Formatter class or string literal used to create the subject + * line. The empty string can be used to specify no subject. All control + * characters are removed from the subject line. (defaults to {@linkplain + * com.sun.mail.util.logging.CollectorFormatter CollectorFormatter}.) + * + *
  • <handler-name>.pushFilter the name of a + * Filter class used to trigger an early push. + * (defaults to null, no early push) + * + *
  • <handler-name>.pushLevel the level which will + * trigger an early push. (defaults to Level.OFF, only push when + * full) + * + *
  • <handler-name>.verify used to + * verify the Handler configuration prior to a push. + *
      + *
    • If the value is not set, equal to an empty string, or equal to the + * literal null then no settings are verified prior to a push. + *
    • If set to a value of limited then the + * Handler will verify minimal local machine settings. + *
    • If set to a value of local the Handler + * will verify all of settings of the local machine. + *
    • If set to a value of resolve, the Handler + * will verify all local settings and try to resolve the remote host name + * with the domain name server. + *
    • If set to a value of login, the Handler + * will verify all local settings and try to establish a connection with + * the email server. + *
    • If set to a value of remote, the Handler + * will verify all local settings, try to establish a connection with the + * email server, and try to verify the envelope of the email message. + *
    + * If this Handler is only implicitly closed by the + * LogManager, then verification should be turned on. + * (defaults to null, no verify). + *
+ * + *

+ * Normalization: + * The error manager, filters, and formatters when loaded from the LogManager + * are converted into canonical form inside the MailHandler. The pool of + * interned values is limited to each MailHandler object such that no two + * MailHandler objects created by the LogManager will be created sharing + * identical error managers, filters, or formatters. If a filter or formatter + * should not be interned then it is recommended to retain the identity + * equals and identity hashCode methods as the implementation. For a filter or + * formatter to be interned the class must implement the + * {@linkplain java.lang.Object#equals(java.lang.Object) equals} + * and {@linkplain java.lang.Object#hashCode() hashCode} methods. + * The recommended code to use for stateless filters and formatters is: + *

+ * public boolean equals(Object obj) {
+ *     return obj == null ? false : obj.getClass() == getClass();
+ * }
+ *
+ * public int hashCode() {
+ *     return 31 * getClass().hashCode();
+ * }
+ * 
+ * + *

+ * Sorting: + * All LogRecord objects are ordered prior to formatting if this + * Handler has a non null comparator. Developers might be + * interested in sorting the formatted email by thread id, time, and sequence + * properties of a LogRecord. Where as system administrators might + * be interested in sorting the formatted email by thrown, level, time, and + * sequence properties of a LogRecord. If comparator for this + * handler is null then the order is unspecified. + * + *

+ * Formatting: + * The main message body is formatted using the Formatter returned + * by getFormatter(). Only records that pass the filter returned + * by getFilter() will be included in the message body. The + * subject Formatter will see all LogRecord objects + * that were published regardless of the current Filter. The MIME + * type of the message body can be + * {@linkplain FileTypeMap#setDefaultFileTypeMap overridden} + * by adding a MIME {@linkplain MimetypesFileTypeMap entry} using the simple + * class name of the body formatter as the file extension. The MIME type of the + * attachments can be overridden by changing the attachment file name extension + * or by editing the default MIME entry for a specific file name extension. + * + *

+ * Attachments: + * This Handler allows multiple attachments per each email message. + * The presence of an attachment formatter will change the content type of the + * email message to a multi-part message. The attachment order maps directly to + * the array index order in this Handler with zero index being the + * first attachment. The number of attachment formatters controls the number of + * attachments per email and the content type of each attachment. The + * attachment filters determine if a LogRecord will be included in + * an attachment. If an attachment filter is null then all records + * are included for that attachment. Attachments without content will be + * omitted from email message. The attachment name formatters create the file + * name for an attachment. Custom attachment name formatters can be used to + * generate an attachment name based on the contents of the attachment. + * + *

+ * Push Level and Push Filter: + * The push method, push level, and optional push filter can be used to + * conditionally trigger a push at or prior to full capacity. When a push + * occurs, the current buffer is formatted into an email and is sent to the + * email server. If the push method, push level, or push filter trigger a push + * then the outgoing email is flagged as high importance with urgent priority. + * + *

+ * Buffering: + * Log records that are published are stored in an internal buffer. When this + * buffer reaches capacity the existing records are formatted and sent in an + * email. Any published records can be sent before reaching capacity by + * explictly calling the flush, push, or + * close methods. If a circular buffer is required then this + * handler can be wrapped with a {@linkplain java.util.logging.MemoryHandler} + * typically with an equivalent capacity, level, and push level. + * + *

+ * Error Handling: + * If the transport of an email message fails, the email is converted to + * a {@linkplain javax.mail.internet.MimeMessage#writeTo raw} + * {@linkplain java.io.ByteArrayOutputStream#toString(java.lang.String) string} + * and is then passed as the msg parameter to + * {@linkplain Handler#reportError reportError} along with the exception + * describing the cause of the failure. This allows custom error managers to + * store, {@linkplain javax.mail.internet.MimeMessage#MimeMessage( + * javax.mail.Session, java.io.InputStream) reconstruct}, and resend the + * original MimeMessage. The message parameter string is not a raw email + * if it starts with value returned from Level.SEVERE.getName(). + * Custom error managers can use the following test to determine if the + * msg parameter from this handler is a raw email: + * + *

+ * public void error(String msg, Exception ex, int code) {
+ *      if (msg == null || msg.length() == 0 || msg.startsWith(Level.SEVERE.getName())) {
+ *          super.error(msg, ex, code);
+ *      } else {
+ *          //The 'msg' parameter is a raw email.
+ *      }
+ * }
+ * 
+ * + * @author Jason Mehrens + * @since JavaMail 1.4.3 + */ +public class MailHandler extends Handler { + /** + * Use the emptyFilterArray method. + */ + private static final Filter[] EMPTY_FILTERS = new Filter[0]; + /** + * Use the emptyFormatterArray method. + */ + private static final Formatter[] EMPTY_FORMATTERS = new Formatter[0]; + /** + * Min byte size for header data. Used for initial arrays sizing. + */ + private static final int MIN_HEADER_SIZE = 1024; + /** + * Cache the off value. + */ + private static final int offValue = Level.OFF.intValue(); + /** + * The action to set the context class loader for use with the Jakarta Mail API. + * Load and pin this before it is loaded in the close method. The field is + * declared as java.security.PrivilegedAction so + * WebappClassLoader.clearReferencesStaticFinal() method will ignore this + * field. + */ + private static final PrivilegedAction MAILHANDLER_LOADER + = new GetAndSetContext(MailHandler.class); + /** + * A thread local mutex used to prevent logging loops. This code has to be + * prepared to deal with unexpected null values since the + * WebappClassLoader.clearReferencesThreadLocals() and + * InnocuousThread.eraseThreadLocals() can remove thread local values. + * The MUTEX has 5 states: + * 1. A null value meaning default state of not publishing. + * 2. MUTEX_PUBLISH on first entry of a push or publish. + * 3. The index of the first filter to accept a log record. + * 4. MUTEX_REPORT when cycle of records is detected. + * 5. MUTEXT_LINKAGE when a linkage error is reported. + */ + private static final ThreadLocal MUTEX = new ThreadLocal<>(); + /** + * The marker object used to report a publishing state. + * This must be less than the body filter index (-1). + */ + private static final Integer MUTEX_PUBLISH = -2; + /** + * The used for the error reporting state. + * This must be less than the PUBLISH state. + */ + private static final Integer MUTEX_REPORT = -4; + /** + * The used for linkage error reporting. + * This must be less than the REPORT state. + */ + private static final Integer MUTEX_LINKAGE = -8; + /** + * Used to turn off security checks. + */ + private volatile boolean sealed; + /** + * Determines if we are inside of a push. + * Makes the handler properties read-only during a push. + */ + private boolean isWriting; + /** + * Holds all of the email server properties. + */ + private Properties mailProps; + /** + * Holds the authenticator required to login to the email server. + */ + private Authenticator auth; + /** + * Holds the session object used to generate emails. + * Sessions can be shared by multiple threads. + * See JDK-6228391 and K 6278. + */ + private Session session; + /** + * A mapping of log record to matching filter index. Negative one is used + * to track the body filter. Zero and greater is used to track the + * attachment parts. All indexes less than or equal to the matched value + * have already seen the given log record. + */ + private int[] matched; + /** + * Holds all of the log records that will be used to create the email. + */ + private LogRecord[] data; + /** + * The number of log records in the buffer. + */ + private int size; + /** + * The maximum number of log records to format per email. + * Used to roughly bound the size of an email. + * Every time the capacity is reached, the handler will push. + * The capacity will be negative if this handler is closed. + * Negative values are used to ensure all records are pushed. + */ + private int capacity; + /** + * Used to order all log records prior to formatting. The main email body + * and all attachments use the order determined by this comparator. If no + * comparator is present the log records will be in no specified order. + */ + private Comparator comparator; + /** + * Holds the formatter used to create the subject line of the email. + * A subject formatter is not required for the email message. + * All published records pass through the subject formatter. + */ + private Formatter subjectFormatter; + /** + * Holds the push level for this handler. + * This is only required if an email must be sent prior to shutdown + * or before the buffer is full. + */ + private Level pushLevel; + /** + * Holds the push filter for trigger conditions requiring an early push. + * Only gets called if the given log record is greater than or equal + * to the push level and the push level is not Level.OFF. + */ + private Filter pushFilter; + /** + * Holds the entry and body filter for this handler. + * There is no way to un-seal the super handler. + */ + private volatile Filter filter; + /** + * Holds the level for this handler. + * There is no way to un-seal the super handler. + */ + private volatile Level logLevel = Level.ALL; + /** + * Holds the filters for each attachment. Filters are optional for + * each attachment. This is declared volatile because this is treated as + * copy-on-write. The VO_VOLATILE_REFERENCE_TO_ARRAY warning is a false + * positive. + */ + @SuppressWarnings("VolatileArrayField") + private volatile Filter[] attachmentFilters; + /** + * Holds the encoding name for this handler. + * There is no way to un-seal the super handler. + */ + private String encoding; + /** + * Holds the entry and body filter for this handler. + * There is no way to un-seal the super handler. + */ + private Formatter formatter; + /** + * Holds the formatters that create the content for each attachment. + * Each formatter maps directly to an attachment. The formatters + * getHead, format, and getTail methods are only called if one or more + * log records pass through the attachment filters. + */ + private Formatter[] attachmentFormatters; + /** + * Holds the formatters that create the file name for each attachment. + * Each formatter must produce a non null and non empty name. + * The final file name will be the concatenation of one getHead call, plus + * all of the format calls, plus one getTail call. + */ + private Formatter[] attachmentNames; + /** + * Used to override the content type for the body and set the content type + * for each attachment. + */ + private FileTypeMap contentTypes; + /** + * Holds the error manager for this handler. + * There is no way to un-seal the super handler. + */ + private volatile ErrorManager errorManager = defaultErrorManager(); + + /** + * Creates a MailHandler that is configured by the + * LogManager configuration properties. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + */ + public MailHandler() { + init((Properties) null); + sealed = true; + checkAccess(); + } + + /** + * Creates a MailHandler that is configured by the + * LogManager configuration properties but overrides the + * LogManager capacity with the given capacity. + * @param capacity of the internal buffer. + * @throws IllegalArgumentException if capacity less than one. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + */ + public MailHandler(final int capacity) { + init((Properties) null); + sealed = true; + setCapacity0(capacity); + } + + /** + * Creates a mail handler with the given mail properties. + * The key/value pairs are defined in the Java Mail API + * documentation. This Handler will also search the + * LogManager for defaults if needed. + * @param props a non null properties object. + * @throws NullPointerException if props is null. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + */ + public MailHandler(final Properties props) { + if (props == null) { + throw new NullPointerException(); + } + init(props); + sealed = true; + setMailProperties0(props); + } + + /** + * Check if this Handler would actually log a given + * LogRecord into its internal buffer. + *

+ * This method checks if the LogRecord has an appropriate level + * and whether it satisfies any Filter including any + * attachment filters. + * However it does not check whether the LogRecord would + * result in a "push" of the buffer contents. + *

+ * @param record a LogRecord or null. + * @return true if the LogRecord would be logged. + */ + @Override + public boolean isLoggable(final LogRecord record) { + if (record == null) { //JDK-8233979 + return false; + } + + int levelValue = getLevel().intValue(); + if (record.getLevel().intValue() < levelValue || levelValue == offValue) { + return false; + } + + Filter body = getFilter(); + if (body == null || body.isLoggable(record)) { + setMatchedPart(-1); + return true; + } + + return isAttachmentLoggable(record); + } + + /** + * Stores a LogRecord in the internal buffer. + *

+ * The isLoggable method is called to check if the given log + * record is loggable. If the given record is loggable, it is copied into + * an internal buffer. Then the record's level property is compared with + * the push level. If the given level of the LogRecord + * is greater than or equal to the push level then the push filter is + * called. If no push filter exists, the push filter returns true, + * or the capacity of the internal buffer has been reached then all buffered + * records are formatted into one email and sent to the server. + * + * @param record description of the log event or null. + */ + @Override + public void publish(final LogRecord record) { + /** + * It is possible for the handler to be closed after the + * call to isLoggable. In that case, the current thread + * will push to ensure that all published records are sent. + * See close(). + */ + + if (tryMutex()) { + try { + if (isLoggable(record)) { + if (record != null) { + record.getSourceMethodName(); //Infer caller. + publish0(record); + } else { //Override of isLoggable is broken. + reportNullError(ErrorManager.WRITE_FAILURE); + } + } + } catch (final LinkageError JDK8152515) { + reportLinkageError(JDK8152515, ErrorManager.WRITE_FAILURE); + } finally { + releaseMutex(); + } + } else { + reportUnPublishedError(record); + } + } + + /** + * Performs the publish after the record has been filtered. + * @param record the record which must not be null. + * @since JavaMail 1.4.5 + */ + private void publish0(final LogRecord record) { + Message msg; + boolean priority; + synchronized (this) { + if (size == data.length && size < capacity) { + grow(); + } + + if (size < data.length) { + //assert data.length == matched.length; + matched[size] = getMatchedPart(); + data[size] = record; + ++size; //Be nice to client compiler. + priority = isPushable(record); + if (priority || size >= capacity) { + msg = writeLogRecords(ErrorManager.WRITE_FAILURE); + } else { + msg = null; + } + } else { + priority = false; + msg = null; + } + } + + if (msg != null) { + send(msg, priority, ErrorManager.WRITE_FAILURE); + } + } + + /** + * Report to the error manager that a logging loop was detected and + * we are going to break the cycle of messages. It is possible that + * a custom error manager could continue the cycle in which case + * we will stop trying to report errors. + * @param record the record or null. + * @since JavaMail 1.4.6 + */ + private void reportUnPublishedError(LogRecord record) { + final Integer idx = MUTEX.get(); + if (idx == null || idx > MUTEX_REPORT) { + MUTEX.set(MUTEX_REPORT); + try { + final String msg; + if (record != null) { + final Formatter f = createSimpleFormatter(); + msg = "Log record " + record.getSequenceNumber() + + " was not published. " + + head(f) + format(f, record) + tail(f, ""); + } else { + msg = null; + } + Exception e = new IllegalStateException( + "Recursive publish detected by thread " + + Thread.currentThread()); + reportError(msg, e, ErrorManager.WRITE_FAILURE); + } finally { + if (idx != null) { + MUTEX.set(idx); + } else { + MUTEX.remove(); + } + } + } + } + + /** + * Used to detect reentrance by the current thread to the publish method. + * This mutex is thread local scope and will not block other threads. + * The state is advanced on if the current thread is in a reset state. + * @return true if the mutex was acquired. + * @since JavaMail 1.4.6 + */ + private boolean tryMutex() { + if (MUTEX.get() == null) { + MUTEX.set(MUTEX_PUBLISH); + return true; + } else { + return false; + } + } + + /** + * Releases the mutex held by the current thread. + * This mutex is thread local scope and will not block other threads. + * @since JavaMail 1.4.6 + */ + private void releaseMutex() { + MUTEX.remove(); + } + + /** + * This is used to get the filter index from when {@code isLoggable} and + * {@code isAttachmentLoggable} was invoked by {@code publish} method. + * + * @return the filter index or MUTEX_PUBLISH if unknown. + * @since JavaMail 1.5.5 + * @throws NullPointerException if tryMutex was not called. + */ + private int getMatchedPart() { + //assert Thread.holdsLock(this); + Integer idx = MUTEX.get(); + if (idx == null || idx >= readOnlyAttachmentFilters().length) { + idx = MUTEX_PUBLISH; + } + return idx; + } + + /** + * This is used to record the filter index when {@code isLoggable} and + * {@code isAttachmentLoggable} was invoked by {@code publish} method. + * + * @param index the filter index. + * @since JavaMail 1.5.5 + */ + private void setMatchedPart(int index) { + if (MUTEX_PUBLISH.equals(MUTEX.get())) { + MUTEX.set(index); + } + } + + /** + * Clear previous matches when the filters are modified and there are + * existing log records that were matched. + * @param index the lowest filter index to clear. + * @since JavaMail 1.5.5 + */ + private void clearMatches(int index) { + assert Thread.holdsLock(this); + for (int r = 0; r < size; ++r) { + if (matched[r] >= index) { + matched[r] = MUTEX_PUBLISH; + } + } + } + + /** + * A callback method for when this object is about to be placed into + * commission. This contract is defined by the + * {@code org.glassfish.hk2.api.PostConstruct} interface. If this class is + * loaded via a lifecycle managed environment other than HK2 then it is + * recommended that this method is called either directly or through + * extending this class to signal that this object is ready for use. + * + * @since JavaMail 1.5.3 + */ + //@javax.annotation.PostConstruct + public void postConstruct() { + } + + /** + * A callback method for when this object is about to be decommissioned. + * This contract is defined by the {@code org.glassfish.hk2.api.PreDestory} + * interface. If this class is loaded via a lifecycle managed environment + * other than HK2 then it is recommended that this method is called either + * directly or through extending this class to signal that this object will + * be destroyed. + * + * @since JavaMail 1.5.3 + */ + //@javax.annotation.PreDestroy + public void preDestroy() { + /** + * Close can require permissions so just trigger a push. + */ + push(false, ErrorManager.CLOSE_FAILURE); + } + + /** + * Pushes any buffered records to the email server as high importance with + * urgent priority. The internal buffer is then cleared. Does nothing if + * called from inside a push. + * @see #flush() + */ + public void push() { + push(true, ErrorManager.FLUSH_FAILURE); + } + + /** + * Pushes any buffered records to the email server as normal priority. + * The internal buffer is then cleared. Does nothing if called from inside + * a push. + * @see #push() + */ + @Override + public void flush() { + push(false, ErrorManager.FLUSH_FAILURE); + } + + /** + * Prevents any other records from being published. + * Pushes any buffered records to the email server as normal priority. + * The internal buffer is then cleared. Once this handler is closed it + * will remain closed. + *

+ * If this Handler is only implicitly closed by the + * LogManager, then verification should + * be turned on. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @see #flush() + */ + @Override + public void close() { + try { + checkAccess(); //Ensure setLevel works before clearing the buffer. + Message msg = null; + synchronized (this) { + try { + msg = writeLogRecords(ErrorManager.CLOSE_FAILURE); + } finally { //Change level after formatting. + this.logLevel = Level.OFF; + /** + * The sign bit of the capacity is set to ensure that + * records that have passed isLoggable, but have yet to be + * added to the internal buffer, are immediately pushed as + * an email. + */ + if (this.capacity > 0) { + this.capacity = -this.capacity; + } + + //Ensure not inside a push. + if (size == 0 && data.length != 1) { + this.data = new LogRecord[1]; + this.matched = new int[this.data.length]; + } + } + } + + if (msg != null) { + send(msg, false, ErrorManager.CLOSE_FAILURE); + } + } catch (final LinkageError JDK8152515) { + reportLinkageError(JDK8152515, ErrorManager.CLOSE_FAILURE); + } + } + + /** + * Set the log level specifying which message levels will be + * logged by this Handler. Message levels lower than this + * value will be discarded. + * @param newLevel the new value for the log level + * @throws NullPointerException if newLevel is + * null. + * @throws SecurityException if a security manager exists and + * the caller does not have + * LoggingPermission("control"). + */ + @Override + public void setLevel(final Level newLevel) { + if (newLevel == null) { + throw new NullPointerException(); + } + checkAccess(); + + //Don't allow a closed handler to be opened (half way). + synchronized (this) { //Wait for writeLogRecords. + if (this.capacity > 0) { + this.logLevel = newLevel; + } + } + } + + /** + * Get the log level specifying which messages will be logged by this + * Handler. Message levels lower than this level will be + * discarded. + * + * @return the level of messages being logged. + */ + @Override + public Level getLevel() { + return logLevel; //Volatile access. + } + + /** + * Retrieves the ErrorManager for this Handler. + * + * @return the ErrorManager for this Handler + * @throws SecurityException if a security manager exists and if the caller + * does not have LoggingPermission("control"). + */ + @Override + public ErrorManager getErrorManager() { + checkAccess(); + return this.errorManager; //Volatile access. + } + + /** + * Define an ErrorManager for this Handler. + *

+ * The ErrorManager's "error" method will be invoked if any errors occur + * while using this Handler. + * + * @param em the new ErrorManager + * @throws SecurityException if a security manager exists and if the + * caller does not have LoggingPermission("control"). + * @throws NullPointerException if the given error manager is null. + */ + @Override + public void setErrorManager(final ErrorManager em) { + checkAccess(); + setErrorManager0(em); + } + + /** + * Sets the error manager on this handler and the super handler. In secure + * environments the super call may not be allowed which is not a failure + * condition as it is an attempt to free the unused handler error manager. + * + * @param em a non null error manager. + * @throws NullPointerException if the given error manager is null. + * @since JavaMail 1.5.6 + */ + private void setErrorManager0(final ErrorManager em) { + if (em == null) { + throw new NullPointerException(); + } + try { + synchronized (this) { //Wait for writeLogRecords. + this.errorManager = em; + super.setErrorManager(em); //Try to free super error manager. + } + } catch (RuntimeException | LinkageError ignore) { + } + } + + /** + * Get the current Filter for this Handler. + * + * @return a Filter object (may be null) + */ + @Override + public Filter getFilter() { + return this.filter; //Volatile access. + } + + /** + * Set a Filter to control output on this Handler. + *

+ * For each call of publish the Handler will call + * this Filter (if it is non-null) to check if the + * LogRecord should be published or discarded. + * + * @param newFilter a Filter object (may be null) + * @throws SecurityException if a security manager exists and if the caller + * does not have LoggingPermission("control"). + */ + @Override + public void setFilter(final Filter newFilter) { + checkAccess(); + synchronized (this) { //Wait for writeLogRecords. + if (newFilter != filter) { + clearMatches(-1); + } + this.filter = newFilter; //Volatile access. + } + } + + /** + * Return the character encoding for this Handler. + * + * @return The encoding name. May be null, which indicates the default + * encoding should be used. + */ + @Override + public synchronized String getEncoding() { + return this.encoding; + } + + /** + * Set the character encoding used by this Handler. + *

+ * The encoding should be set before any LogRecords are written + * to the Handler. + * + * @param encoding The name of a supported character encoding. May be + * null, to indicate the default platform encoding. + * @throws SecurityException if a security manager exists and if the caller + * does not have LoggingPermission("control"). + * @throws UnsupportedEncodingException if the named encoding is not + * supported. + */ + @Override + public void setEncoding(String encoding) throws UnsupportedEncodingException { + checkAccess(); + setEncoding0(encoding); + } + + /** + * Set the character encoding used by this handler. This method does not + * check permissions of the caller. + * + * @param e any encoding name or null for the default. + * @throws UnsupportedEncodingException if the given encoding is not supported. + */ + private void setEncoding0(String e) throws UnsupportedEncodingException { + if (e != null) { + try { + if (!java.nio.charset.Charset.isSupported(e)) { + throw new UnsupportedEncodingException(e); + } + } catch (java.nio.charset.IllegalCharsetNameException icne) { + throw new UnsupportedEncodingException(e); + } + } + + synchronized (this) { //Wait for writeLogRecords. + this.encoding = e; + } + } + + /** + * Return the Formatter for this Handler. + * + * @return the Formatter (may be null). + */ + @Override + public synchronized Formatter getFormatter() { + return this.formatter; + } + + /** + * Set a Formatter. This Formatter will be used + * to format LogRecords for this Handler. + *

+ * Some Handlers may not use Formatters, in which + * case the Formatter will be remembered, but not used. + *

+ * @param newFormatter the Formatter to use (may not be null) + * @throws SecurityException if a security manager exists and if the caller + * does not have LoggingPermission("control"). + * @throws NullPointerException if the given formatter is null. + */ + @Override + public synchronized void setFormatter(Formatter newFormatter) throws SecurityException { + checkAccess(); + if (newFormatter == null) { + throw new NullPointerException(); + } + this.formatter = newFormatter; + } + + /** + * Gets the push level. The default is Level.OFF meaning that + * this Handler will only push when the internal buffer is full. + * @return the push level. + */ + public final synchronized Level getPushLevel() { + return this.pushLevel; + } + + /** + * Sets the push level. This level is used to trigger a push so that + * all pending records are formatted and sent to the email server. When + * the push level triggers a send, the resulting email is flagged as + * high importance with urgent priority. + * @param level Level object. + * @throws NullPointerException if level is null. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws IllegalStateException if called from inside a push. + */ + public final synchronized void setPushLevel(final Level level) { + checkAccess(); + if (level == null) { + throw new NullPointerException(); + } + + if (isWriting) { + throw new IllegalStateException(); + } + this.pushLevel = level; + } + + /** + * Gets the push filter. The default is null. + * @return the push filter or null. + */ + public final synchronized Filter getPushFilter() { + return this.pushFilter; + } + + /** + * Sets the push filter. This filter is only called if the given + * LogRecord level was greater than the push level. If this + * filter returns true, all pending records are formatted and + * sent to the email server. When the push filter triggers a send, the + * resulting email is flagged as high importance with urgent priority. + * @param filter push filter or null + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws IllegalStateException if called from inside a push. + */ + public final synchronized void setPushFilter(final Filter filter) { + checkAccess(); + if (isWriting) { + throw new IllegalStateException(); + } + this.pushFilter = filter; + } + + /** + * Gets the comparator used to order all LogRecord objects + * prior to formatting. If null then the order is unspecified. + * @return the LogRecord comparator. + */ + public final synchronized Comparator getComparator() { + return this.comparator; + } + + /** + * Sets the comparator used to order all LogRecord objects + * prior to formatting. If null then the order is unspecified. + * @param c the LogRecord comparator. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws IllegalStateException if called from inside a push. + */ + public final synchronized void setComparator(Comparator c) { + checkAccess(); + if (isWriting) { + throw new IllegalStateException(); + } + this.comparator = c; + } + + /** + * Gets the number of log records the internal buffer can hold. When + * capacity is reached, Handler will format all + * LogRecord objects into one email message. + * @return the capacity. + */ + public final synchronized int getCapacity() { + assert capacity != Integer.MIN_VALUE && capacity != 0 : capacity; + return Math.abs(capacity); + } + + /** + * Gets the Authenticator used to login to the email server. + * @return an Authenticator or null if none is + * required. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + */ + public final synchronized Authenticator getAuthenticator() { + checkAccess(); + return this.auth; + } + + /** + * Sets the Authenticator used to login to the email server. + * @param auth an Authenticator object or null if none is + * required. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws IllegalStateException if called from inside a push. + */ + public final void setAuthenticator(final Authenticator auth) { + this.setAuthenticator0(auth); + } + + /** + * Sets the Authenticator used to login to the email server. + * @param password a password, empty array can be used to only supply a + * user name set by mail.user property, or null if no + * credentials are required. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws IllegalStateException if called from inside a push. + * @see String#toCharArray() + * @since JavaMail 1.4.6 + */ + public final void setAuthenticator(final char... password) { + if (password == null) { + setAuthenticator0((Authenticator) null); + } else { + setAuthenticator0(DefaultAuthenticator.of(new String(password))); + } + } + + /** + * A private hook to handle possible future overrides. See public method. + * @param auth see public method. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws IllegalStateException if called from inside a push. + */ + private void setAuthenticator0(final Authenticator auth) { + checkAccess(); + + Session settings; + synchronized (this) { + if (isWriting) { + throw new IllegalStateException(); + } + this.auth = auth; + settings = updateSession(); + } + verifySettings(settings); + } + + /** + * Sets the mail properties used for the session. The key/value pairs + * are defined in the Java Mail API documentation. This + * Handler will also search the LogManager for + * defaults if needed. + * @param props a non null properties object. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws NullPointerException if props is null. + * @throws IllegalStateException if called from inside a push. + */ + public final void setMailProperties(Properties props) { + this.setMailProperties0(props); + } + + /** + * A private hook to handle overrides when the public method is declared + * non final. See public method for details. + * @param props see public method. + */ + private void setMailProperties0(Properties props) { + checkAccess(); + props = (Properties) props.clone(); //Allow subclass. + Session settings; + synchronized (this) { + if (isWriting) { + throw new IllegalStateException(); + } + this.mailProps = props; + settings = updateSession(); + } + verifySettings(settings); + } + + /** + * Gets a copy of the mail properties used for the session. + * @return a non null properties object. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + */ + public final Properties getMailProperties() { + checkAccess(); + final Properties props; + synchronized (this) { + props = this.mailProps; + } + return (Properties) props.clone(); + } + + /** + * Gets the attachment filters. If the attachment filter does not + * allow any LogRecord to be formatted, the attachment may + * be omitted from the email. + * @return a non null array of attachment filters. + */ + public final Filter[] getAttachmentFilters() { + return readOnlyAttachmentFilters().clone(); + } + + /** + * Sets the attachment filters. + * @param filters a non null array of filters. A + * null index value is allowed. A null value + * means that all records are allowed for the attachment at that index. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws NullPointerException if filters is null + * @throws IndexOutOfBoundsException if the number of attachment + * name formatters do not match the number of attachment formatters. + * @throws IllegalStateException if called from inside a push. + */ + public final void setAttachmentFilters(Filter... filters) { + checkAccess(); + if (filters.length == 0) { + filters = emptyFilterArray(); + } else { + filters = Arrays.copyOf(filters, filters.length, Filter[].class); + } + synchronized (this) { + if (this.attachmentFormatters.length != filters.length) { + throw attachmentMismatch(this.attachmentFormatters.length, filters.length); + } + + if (isWriting) { + throw new IllegalStateException(); + } + + if (size != 0) { + for (int i = 0; i < filters.length; ++i) { + if (filters[i] != attachmentFilters[i]) { + clearMatches(i); + break; + } + } + } + this.attachmentFilters = filters; + } + } + + /** + * Gets the attachment formatters. This Handler is using + * attachments only if the returned array length is non zero. + * @return a non null array of formatters. + */ + public final Formatter[] getAttachmentFormatters() { + Formatter[] formatters; + synchronized (this) { + formatters = this.attachmentFormatters; + } + return formatters.clone(); + } + + /** + * Sets the attachment Formatter object for this handler. + * The number of formatters determines the number of attachments per + * email. This method should be the first attachment method called. + * To remove all attachments, call this method with empty array. + * @param formatters a non null array of formatters. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws NullPointerException if the given array or any array index is + * null. + * @throws IllegalStateException if called from inside a push. + */ + public final void setAttachmentFormatters(Formatter... formatters) { + checkAccess(); + if (formatters.length == 0) { //Null check and length check. + formatters = emptyFormatterArray(); + } else { + formatters = Arrays.copyOf(formatters, + formatters.length, Formatter[].class); + for (int i = 0; i < formatters.length; ++i) { + if (formatters[i] == null) { + throw new NullPointerException(atIndexMsg(i)); + } + } + } + + synchronized (this) { + if (isWriting) { + throw new IllegalStateException(); + } + + this.attachmentFormatters = formatters; + this.alignAttachmentFilters(); + this.alignAttachmentNames(); + } + } + + /** + * Gets the attachment name formatters. + * If the attachment names were set using explicit names then + * the names can be returned by calling toString on each + * attachment name formatter. + * @return non null array of attachment name formatters. + */ + public final Formatter[] getAttachmentNames() { + final Formatter[] formatters; + synchronized (this) { + formatters = this.attachmentNames; + } + return formatters.clone(); + } + + /** + * Sets the attachment file name for each attachment. All control + * characters are removed from the attachment names. + * This method will create a set of custom formatters. + * @param names an array of names. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws IndexOutOfBoundsException if the number of attachment + * names do not match the number of attachment formatters. + * @throws IllegalArgumentException if any name is empty. + * @throws NullPointerException if any given array or name is + * null. + * @throws IllegalStateException if called from inside a push. + * @see Character#isISOControl(char) + * @see Character#isISOControl(int) + */ + public final void setAttachmentNames(final String... names) { + checkAccess(); + + final Formatter[] formatters; + if (names.length == 0) { + formatters = emptyFormatterArray(); + } else { + formatters = new Formatter[names.length]; + } + + for (int i = 0; i < names.length; ++i) { + final String name = names[i]; + if (name != null) { + if (name.length() > 0) { + formatters[i] = TailNameFormatter.of(name); + } else { + throw new IllegalArgumentException(atIndexMsg(i)); + } + } else { + throw new NullPointerException(atIndexMsg(i)); + } + } + + synchronized (this) { + if (this.attachmentFormatters.length != names.length) { + throw attachmentMismatch(this.attachmentFormatters.length, names.length); + } + + if (isWriting) { + throw new IllegalStateException(); + } + this.attachmentNames = formatters; + } + } + + /** + * Sets the attachment file name formatters. The format method of each + * attachment formatter will see only the LogRecord objects + * that passed its attachment filter during formatting. The format method + * will typically return an empty string. Instead of being used to format + * records, it is used to gather information about the contents of an + * attachment. The getTail method should be used to construct + * the attachment file name and reset any formatter collected state. All + * control characters will be removed from the output of the formatter. The + * toString method of the given formatter should be overridden + * to provide a useful attachment file name, if possible. + * @param formatters and array of attachment name formatters. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws IndexOutOfBoundsException if the number of attachment + * name formatters do not match the number of attachment formatters. + * @throws NullPointerException if any given array or name is + * null. + * @throws IllegalStateException if called from inside a push. + * @see Character#isISOControl(char) + * @see Character#isISOControl(int) + */ + public final void setAttachmentNames(Formatter... formatters) { + checkAccess(); + + if (formatters.length == 0) { + formatters = emptyFormatterArray(); + } else { + formatters = Arrays.copyOf(formatters, formatters.length, + Formatter[].class); + } + + for (int i = 0; i < formatters.length; ++i) { + if (formatters[i] == null) { + throw new NullPointerException(atIndexMsg(i)); + } + } + + synchronized (this) { + if (this.attachmentFormatters.length != formatters.length) { + throw attachmentMismatch(this.attachmentFormatters.length, + formatters.length); + } + + if (isWriting) { + throw new IllegalStateException(); + } + + this.attachmentNames = formatters; + } + } + + /** + * Gets the formatter used to create the subject line. + * If the subject was created using a literal string then + * the toString method can be used to get the subject line. + * @return the formatter. + */ + public final synchronized Formatter getSubject() { + return this.subjectFormatter; + } + + /** + * Sets a literal string for the email subject. All control characters are + * removed from the subject line. + * @param subject a non null string. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws NullPointerException if subject is + * null. + * @throws IllegalStateException if called from inside a push. + * @see Character#isISOControl(char) + * @see Character#isISOControl(int) + */ + public final void setSubject(final String subject) { + if (subject != null) { + this.setSubject(TailNameFormatter.of(subject)); + } else { + checkAccess(); + throw new NullPointerException(); + } + } + + /** + * Sets the subject formatter for email. The format method of the subject + * formatter will see all LogRecord objects that were published + * to this Handler during formatting and will typically return + * an empty string. This formatter is used to gather information to create + * a summary about what information is contained in the email. The + * getTail method should be used to construct the subject and + * reset any formatter collected state. All control characters + * will be removed from the formatter output. The toString + * method of the given formatter should be overridden to provide a useful + * subject, if possible. + * @param format the subject formatter. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws NullPointerException if format is null. + * @throws IllegalStateException if called from inside a push. + * @see Character#isISOControl(char) + * @see Character#isISOControl(int) + */ + public final void setSubject(final Formatter format) { + checkAccess(); + if (format == null) { + throw new NullPointerException(); + } + + synchronized (this) { + if (isWriting) { + throw new IllegalStateException(); + } + this.subjectFormatter = format; + } + } + + /** + * Protected convenience method to report an error to this Handler's + * ErrorManager. This method will prefix all non null error messages with + * Level.SEVERE.getName(). This allows the receiving error + * manager to determine if the msg parameter is a simple error + * message or a raw email message. + * @param msg a descriptive string (may be null) + * @param ex an exception (may be null) + * @param code an error code defined in ErrorManager + */ + @Override + protected void reportError(String msg, Exception ex, int code) { + try { + if (msg != null) { + errorManager.error(Level.SEVERE.getName() + .concat(": ").concat(msg), ex, code); + } else { + errorManager.error(null, ex, code); + } + } catch (RuntimeException | LinkageError GLASSFISH_21258) { + reportLinkageError(GLASSFISH_21258, code); + } + } + + /** + * Calls log manager checkAccess if this is sealed. + */ + private void checkAccess() { + if (sealed) { + LogManagerProperties.checkLogManagerAccess(); + } + } + + /** + * Determines the mimeType of a formatter from the getHead call. + * This could be made protected, or a new class could be created to do + * this type of conversion. Currently, this is only used for the body + * since the attachments are computed by filename. + * Package-private for unit testing. + * @param chunk any char sequence or null. + * @return return the mime type or null for text/plain. + */ + final String contentTypeOf(CharSequence chunk) { + if (!isEmpty(chunk)) { + final int MAX_CHARS = 25; + if (chunk.length() > MAX_CHARS) { + chunk = chunk.subSequence(0, MAX_CHARS); + } + try { + final String charset = getEncodingName(); + final byte[] b = chunk.toString().getBytes(charset); + final ByteArrayInputStream in = new ByteArrayInputStream(b); + assert in.markSupported() : in.getClass().getName(); + return URLConnection.guessContentTypeFromStream(in); + } catch (final IOException IOE) { + reportError(IOE.getMessage(), IOE, ErrorManager.FORMAT_FAILURE); + } + } + return null; //text/plain + } + + /** + * Determines the mimeType of a formatter by the class name. This method + * avoids calling getHead and getTail of content formatters during verify + * because they might trigger side effects or excessive work. The name + * formatters and subject are usually safe to call. + * Package-private for unit testing. + * + * @param f the formatter or null. + * @return return the mime type or null, meaning text/plain. + * @since JavaMail 1.5.6 + */ + final String contentTypeOf(final Formatter f) { + assert Thread.holdsLock(this); + if (f != null) { + String type = getContentType(f.getClass().getName()); + if (type != null) { + return type; + } + + for (Class k = f.getClass(); k != Formatter.class; + k = k.getSuperclass()) { + String name; + try { + name = k.getSimpleName(); + } catch (final InternalError JDK8057919) { + name = k.getName(); + } + name = name.toLowerCase(Locale.ENGLISH); + for (int idx = name.indexOf('$') + 1; + (idx = name.indexOf("ml", idx)) > -1; idx += 2) { + if (idx > 0) { + if (name.charAt(idx - 1) == 'x') { + return "application/xml"; + } + if (idx > 1 && name.charAt(idx - 2) == 'h' + && name.charAt(idx - 1) == 't') { + return "text/html"; + } + } + } + } + } + return null; + } + + /** + * Determines if the given throwable is a no content exception. It is + * assumed Transport.sendMessage will call Message.writeTo so we need to + * ignore any exceptions that could be layered on top of that call chain to + * infer that sendMessage is failing because of writeTo. Package-private + * for unit testing. + * @param msg the message without content. + * @param t the throwable chain to test. + * @return true if the throwable is a missing content exception. + * @throws NullPointerException if any of the arguments are null. + * @since JavaMail 1.4.5 + */ + @SuppressWarnings({"UseSpecificCatch", "ThrowableResultIgnored"}) + final boolean isMissingContent(Message msg, Throwable t) { + final Object ccl = getAndSetContextClassLoader(MAILHANDLER_LOADER); + try { + msg.writeTo(new ByteArrayOutputStream(MIN_HEADER_SIZE)); + } catch (final RuntimeException RE) { + throw RE; //Avoid catch all. + } catch (final Exception noContent) { + final String txt = noContent.getMessage(); + if (!isEmpty(txt)) { + int limit = 0; + while (t != null) { + if (noContent.getClass() == t.getClass() + && txt.equals(t.getMessage())) { + return true; + } + + //Not all Jakarta Mail implementations support JDK 1.4 + //exception chaining. + final Throwable cause = t.getCause(); + if (cause == null && t instanceof MessagingException) { + t = ((MessagingException) t).getNextException(); + } else { + t = cause; + } + + //Deal with excessive cause chains and cyclic throwables. + if (++limit == (1 << 16)) { + break; //Give up. + } + } + } + } finally { + getAndSetContextClassLoader(ccl); + } + return false; + } + + /** + * Converts a mime message to a raw string or formats the reason + * why message can't be changed to raw string and reports it. + * @param msg the mime message. + * @param ex the original exception. + * @param code the ErrorManager code. + * @since JavaMail 1.4.5 + */ + @SuppressWarnings("UseSpecificCatch") + private void reportError(Message msg, Exception ex, int code) { + try { + try { //Use direct call so we do not prefix raw email. + errorManager.error(toRawString(msg), ex, code); + } catch (final RuntimeException re) { + reportError(toMsgString(re), ex, code); + } catch (final Exception e) { + reportError(toMsgString(e), ex, code); + } + } catch (final LinkageError GLASSFISH_21258) { + reportLinkageError(GLASSFISH_21258, code); + } + } + + /** + * Reports the given linkage error or runtime exception. + * + * The current LogManager code will stop closing all remaining handlers if + * an error is thrown during resetLogger. This is a workaround for + * GLASSFISH-21258 and JDK-8152515. + * @param le the linkage error or a RuntimeException. + * @param code the ErrorManager code. + * @throws NullPointerException if error is null. + * @since JavaMail 1.5.3 + */ + private void reportLinkageError(final Throwable le, final int code) { + if (le == null) { + throw new NullPointerException(String.valueOf(code)); + } + + final Integer idx = MUTEX.get(); + if (idx == null || idx > MUTEX_LINKAGE) { + MUTEX.set(MUTEX_LINKAGE); + try { + Thread.currentThread().getUncaughtExceptionHandler() + .uncaughtException(Thread.currentThread(), le); + } catch (RuntimeException | LinkageError ignore) { + } finally { + if (idx != null) { + MUTEX.set(idx); + } else { + MUTEX.remove(); + } + } + } + } + + /** + * Determines the mimeType from the given file name. + * Used to override the body content type and used for all attachments. + * @param name the file name or class name. + * @return the mime type or null for text/plain. + */ + private String getContentType(final String name) { + assert Thread.holdsLock(this); + final String type = contentTypes.getContentType(name); + if ("application/octet-stream".equalsIgnoreCase(type)) { + return null; //Formatters return strings, default to text/plain. + } + return type; + } + + /** + * Gets the encoding set for this handler, mime encoding, or file encoding. + * @return the java charset name, never null. + * @since JavaMail 1.4.5 + */ + private String getEncodingName() { + String charset = getEncoding(); + if (charset == null) { + charset = MimeUtility.getDefaultJavaCharset(); + } + return charset; + } + + /** + * Set the content for a part using the encoding assigned to the handler. + * @param part the part to assign. + * @param buf the formatted data. + * @param type the mime type or null, meaning text/plain. + * @throws MessagingException if there is a problem. + */ + private void setContent(MimePart part, CharSequence buf, String type) throws MessagingException { + final String charset = getEncodingName(); + if (type != null && !"text/plain".equalsIgnoreCase(type)) { + type = contentWithEncoding(type, charset); + try { + DataSource source = new ByteArrayDataSource(buf.toString(), type); + part.setDataHandler(new DataHandler(source)); + } catch (final IOException IOE) { + reportError(IOE.getMessage(), IOE, ErrorManager.FORMAT_FAILURE); + part.setText(buf.toString(), charset); + } + } else { + part.setText(buf.toString(), MimeUtility.mimeCharset(charset)); + } + } + + /** + * Replaces the charset parameter with the current encoding. + * @param type the content type. + * @param encoding the java charset name. + * @return the type with a specified encoding. + */ + private String contentWithEncoding(String type, String encoding) { + assert encoding != null; + try { + final ContentType ct = new ContentType(type); + ct.setParameter("charset", MimeUtility.mimeCharset(encoding)); + encoding = ct.toString(); //See javax.mail.internet.ContentType. + if (!isEmpty(encoding)) { //Support pre K5687. + type = encoding; + } + } catch (final MessagingException ME) { + reportError(type, ME, ErrorManager.FORMAT_FAILURE); + } + return type; + } + + /** + * Sets the capacity for this handler. This method is kept private + * because we would have to define a public policy for when the size is + * greater than the capacity. + * E.G. do nothing, flush now, truncate now, push now and resize. + * @param newCapacity the max number of records. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws IllegalStateException if called from inside a push. + */ + private synchronized void setCapacity0(final int newCapacity) { + checkAccess(); + if (newCapacity <= 0) { + throw new IllegalArgumentException("Capacity must be greater than zero."); + } + + if (isWriting) { + throw new IllegalStateException(); + } + + if (this.capacity < 0) { //If closed, remain closed. + this.capacity = -newCapacity; + } else { + this.capacity = newCapacity; + } + } + + /** + * Gets the attachment filters using a happens-before relationship between + * this method and setAttachmentFilters. The attachment filters are treated + * as copy-on-write, so the returned array must never be modified or + * published outside this class. + * @return a read only array of filters. + */ + private Filter[] readOnlyAttachmentFilters() { + return this.attachmentFilters; + } + + /** + * Factory for empty formatter arrays. + * @return an empty array. + */ + private static Formatter[] emptyFormatterArray() { + return EMPTY_FORMATTERS; + } + + /** + * Factory for empty filter arrays. + * @return an empty array. + */ + private static Filter[] emptyFilterArray() { + return EMPTY_FILTERS; + } + + /** + * Expand or shrink the attachment name formatters with the attachment + * formatters. + * @return true if size was changed. + */ + private boolean alignAttachmentNames() { + assert Thread.holdsLock(this); + boolean fixed = false; + final int expect = this.attachmentFormatters.length; + final int current = this.attachmentNames.length; + if (current != expect) { + this.attachmentNames = Arrays.copyOf(attachmentNames, expect, + Formatter[].class); + fixed = current != 0; + } + + //Copy of zero length array is cheap, warm up copyOf. + if (expect == 0) { + this.attachmentNames = emptyFormatterArray(); + assert this.attachmentNames.length == 0; + } else { + for (int i = 0; i < expect; ++i) { + if (this.attachmentNames[i] == null) { + this.attachmentNames[i] = TailNameFormatter.of( + toString(this.attachmentFormatters[i])); + } + } + } + return fixed; + } + + /** + * Expand or shrink the attachment filters with the attachment formatters. + * @return true if the size was changed. + */ + private boolean alignAttachmentFilters() { + assert Thread.holdsLock(this); + + boolean fixed = false; + final int expect = this.attachmentFormatters.length; + final int current = this.attachmentFilters.length; + if (current != expect) { + this.attachmentFilters = Arrays.copyOf(attachmentFilters, expect, + Filter[].class); + clearMatches(current); + fixed = current != 0; + + //Array elements default to null so skip filling if body filter + //is null. If not null then only assign to expanded elements. + final Filter body = this.filter; + if (body != null) { + for (int i = current; i < expect; ++i) { + this.attachmentFilters[i] = body; + } + } + } + + //Copy of zero length array is cheap, warm up copyOf. + if (expect == 0) { + this.attachmentFilters = emptyFilterArray(); + assert this.attachmentFilters.length == 0; + } + return fixed; + } + + /** + * Sets the size to zero and clears the current buffer. + */ + private void reset() { + assert Thread.holdsLock(this); + if (size < data.length) { + Arrays.fill(data, 0, size, null); + } else { + Arrays.fill(data, null); + } + this.size = 0; + } + + /** + * Expands the internal buffer up to the capacity. + */ + private void grow() { + assert Thread.holdsLock(this); + final int len = data.length; + int newCapacity = len + (len >> 1) + 1; + if (newCapacity > capacity || newCapacity < len) { + newCapacity = capacity; + } + assert len != capacity : len; + this.data = Arrays.copyOf(data, newCapacity, LogRecord[].class); + this.matched = Arrays.copyOf(matched, newCapacity); + } + + /** + * Configures the handler properties from the log manager. + * @param props the given mail properties. Maybe null and are never + * captured by this handler. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + */ + private synchronized void init(final Properties props) { + assert this.errorManager != null; + final String p = getClass().getName(); + this.mailProps = new Properties(); //See method param comments. + final Object ccl = getAndSetContextClassLoader(MAILHANDLER_LOADER); + try { + this.contentTypes = FileTypeMap.getDefaultFileTypeMap(); + } finally { + getAndSetContextClassLoader(ccl); + } + + //Assign any custom error manager first so it can detect all failures. + initErrorManager(p); + + initLevel(p); + initFilter(p); + initCapacity(p); + initAuthenticator(p); + + initEncoding(p); + initFormatter(p); + initComparator(p); + initPushLevel(p); + initPushFilter(p); + + initSubject(p); + + initAttachmentFormaters(p); + initAttachmentFilters(p); + initAttachmentNames(p); + + if (props == null && fromLogManager(p.concat(".verify")) != null) { + verifySettings(initSession()); + } + intern(); //Show verify warnings first. + } + + /** + * Interns the error manager, formatters, and filters contained in this + * handler. The comparator is not interned. This method can only be + * called from init after all of formatters and filters are in a constructed + * and in a consistent state. + * @since JavaMail 1.5.0 + */ + private void intern() { + assert Thread.holdsLock(this); + try { + Object canidate; + Object result; + final Map seen = new HashMap<>(); + try { + intern(seen, this.errorManager); + } catch (final SecurityException se) { + reportError(se.getMessage(), se, ErrorManager.OPEN_FAILURE); + } + + try { + canidate = this.filter; + result = intern(seen, canidate); + if (result != canidate && result instanceof Filter) { + this.filter = (Filter) result; + } + + canidate = this.formatter; + result = intern(seen, canidate); + if (result != canidate && result instanceof Formatter) { + this.formatter = (Formatter) result; + } + } catch (final SecurityException se) { + reportError(se.getMessage(), se, ErrorManager.OPEN_FAILURE); + } + + canidate = this.subjectFormatter; + result = intern(seen, canidate); + if (result != canidate && result instanceof Formatter) { + this.subjectFormatter = (Formatter) result; + } + + canidate = this.pushFilter; + result = intern(seen, canidate); + if (result != canidate && result instanceof Filter) { + this.pushFilter = (Filter) result; + } + + for (int i = 0; i < attachmentFormatters.length; ++i) { + canidate = attachmentFormatters[i]; + result = intern(seen, canidate); + if (result != canidate && result instanceof Formatter) { + attachmentFormatters[i] = (Formatter) result; + } + + canidate = attachmentFilters[i]; + result = intern(seen, canidate); + if (result != canidate && result instanceof Filter) { + attachmentFilters[i] = (Filter) result; + } + + canidate = attachmentNames[i]; + result = intern(seen, canidate); + if (result != canidate && result instanceof Formatter) { + attachmentNames[i] = (Formatter) result; + } + } + } catch (final Exception skip) { + reportError(skip.getMessage(), skip, ErrorManager.OPEN_FAILURE); + } catch (final LinkageError skip) { + reportError(skip.getMessage(), new InvocationTargetException(skip), + ErrorManager.OPEN_FAILURE); + } + } + + /** + * If possible performs an intern of the given object into the + * map. If the object can not be interned the given object is returned. + * @param m the map used to record the interned values. + * @param o the object to try an intern. + * @return the original object or an intern replacement. + * @throws SecurityException if this operation is not allowed by the + * security manager. + * @throws Exception if there is an unexpected problem. + * @since JavaMail 1.5.0 + */ + private Object intern(Map m, Object o) throws Exception { + if (o == null) { + return null; + } + + /** + * The common case is that most objects will not intern. The given + * object has a public no argument constructor or is an instance of a + * TailNameFormatter. TailNameFormatter is safe use as a map key. + * For everything else we create a clone of the given object. + * This is done because of the following: + * 1. Clones can be used to test that a class provides an equals method + * and that the equals method works correctly. + * 2. Calling equals on the given object is assumed to be cheap. + * 3. The intern map can be filtered so it only contains objects that + * can be interned, which reduces the memory footprint. + * 4. Clones are method local garbage. + * 5. Hash code is only called on the clones so bias locking is not + * disabled on the objects the handler will use. + */ + final Object key; + if (o.getClass().getName().equals(TailNameFormatter.class.getName())) { + key = o; + } else { + //This call was already made in the LogManagerProperties so this + //shouldn't trigger loading of any lazy reflection code. + key = o.getClass().getConstructor().newInstance(); + } + + final Object use; + //Check the classloaders of each object avoiding the security manager. + if (key.getClass() == o.getClass()) { + Object found = m.get(key); //Transitive equals test. + if (found == null) { + //Ensure that equals is symmetric to prove intern is safe. + final boolean right = key.equals(o); + final boolean left = o.equals(key); + if (right && left) { + //Assume hashCode is defined at this point. + found = m.put(o, o); + if (found != null) { + reportNonDiscriminating(key, found); + found = m.remove(key); + if (found != o) { + reportNonDiscriminating(key, found); + m.clear(); //Try to restore order. + } + } + } else { + if (right != left) { + reportNonSymmetric(o, key); + } + } + use = o; + } else { + //Check for a discriminating equals method. + if (o.getClass() == found.getClass()) { + use = found; + } else { + reportNonDiscriminating(o, found); + use = o; + } + } + } else { + use = o; + } + return use; + } + + /** + * Factory method used to create a java.util.logging.SimpleFormatter. + * @return a new SimpleFormatter. + * @since JavaMail 1.5.6 + */ + private static Formatter createSimpleFormatter() { + //Don't force the byte code verifier to load the formatter. + return Formatter.class.cast(new SimpleFormatter()); + } + + /** + * Checks a char sequence value for null or empty. + * @param s the char sequence. + * @return true if the given string is null or zero length. + */ + private static boolean isEmpty(final CharSequence s) { + return s == null || s.length() == 0; + } + + /** + * Checks that a string is not empty and not equal to the literal "null". + * @param name the string to check for a value. + * @return true if the string has a valid value. + */ + private static boolean hasValue(final String name) { + return !isEmpty(name) && !"null".equalsIgnoreCase(name); + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initAttachmentFilters(final String p) { + assert Thread.holdsLock(this); + assert this.attachmentFormatters != null; + final String list = fromLogManager(p.concat(".attachment.filters")); + if (!isEmpty(list)) { + final String[] names = list.split(","); + Filter[] a = new Filter[names.length]; + for (int i = 0; i < a.length; ++i) { + names[i] = names[i].trim(); + if (!"null".equalsIgnoreCase(names[i])) { + try { + a[i] = LogManagerProperties.newFilter(names[i]); + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } + } + + this.attachmentFilters = a; + if (alignAttachmentFilters()) { + reportError("Attachment filters.", + attachmentMismatch("Length mismatch."), ErrorManager.OPEN_FAILURE); + } + } else { + this.attachmentFilters = emptyFilterArray(); + alignAttachmentFilters(); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initAttachmentFormaters(final String p) { + assert Thread.holdsLock(this); + final String list = fromLogManager(p.concat(".attachment.formatters")); + if (!isEmpty(list)) { + final Formatter[] a; + final String[] names = list.split(","); + if (names.length == 0) { + a = emptyFormatterArray(); + } else { + a = new Formatter[names.length]; + } + + for (int i = 0; i < a.length; ++i) { + names[i] = names[i].trim(); + if (!"null".equalsIgnoreCase(names[i])) { + try { + a[i] = LogManagerProperties.newFormatter(names[i]); + if (a[i] instanceof TailNameFormatter) { + final Exception CNFE = new ClassNotFoundException(a[i].toString()); + reportError("Attachment formatter.", CNFE, ErrorManager.OPEN_FAILURE); + a[i] = createSimpleFormatter(); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + a[i] = createSimpleFormatter(); + } + } else { + final Exception NPE = new NullPointerException(atIndexMsg(i)); + reportError("Attachment formatter.", NPE, ErrorManager.OPEN_FAILURE); + a[i] = createSimpleFormatter(); + } + } + + this.attachmentFormatters = a; + } else { + this.attachmentFormatters = emptyFormatterArray(); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initAttachmentNames(final String p) { + assert Thread.holdsLock(this); + assert this.attachmentFormatters != null; + + final String list = fromLogManager(p.concat(".attachment.names")); + if (!isEmpty(list)) { + final String[] names = list.split(","); + final Formatter[] a = new Formatter[names.length]; + for (int i = 0; i < a.length; ++i) { + names[i] = names[i].trim(); + if (!"null".equalsIgnoreCase(names[i])) { + try { + try { + a[i] = LogManagerProperties.newFormatter(names[i]); + } catch (ClassNotFoundException + | ClassCastException literal) { + a[i] = TailNameFormatter.of(names[i]); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } else { + final Exception NPE = new NullPointerException(atIndexMsg(i)); + reportError("Attachment names.", NPE, ErrorManager.OPEN_FAILURE); + } + } + + this.attachmentNames = a; + if (alignAttachmentNames()) { //Any null indexes are repaired. + reportError("Attachment names.", + attachmentMismatch("Length mismatch."), ErrorManager.OPEN_FAILURE); + } + } else { + this.attachmentNames = emptyFormatterArray(); + alignAttachmentNames(); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initAuthenticator(final String p) { + assert Thread.holdsLock(this); + String name = fromLogManager(p.concat(".authenticator")); + if (name != null && !"null".equalsIgnoreCase(name)) { + if (name.length() != 0) { + try { + this.auth = LogManagerProperties + .newObjectFrom(name, Authenticator.class); + } catch (final SecurityException SE) { + throw SE; + } catch (final ClassNotFoundException + | ClassCastException literalAuth) { + this.auth = DefaultAuthenticator.of(name); + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } else { //Authenticator is installed to provide the user name. + this.auth = DefaultAuthenticator.of(name); + } + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initLevel(final String p) { + assert Thread.holdsLock(this); + try { + final String val = fromLogManager(p.concat(".level")); + if (val != null) { + logLevel = Level.parse(val); + } else { + logLevel = Level.WARNING; + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final RuntimeException RE) { + reportError(RE.getMessage(), RE, ErrorManager.OPEN_FAILURE); + logLevel = Level.WARNING; + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initFilter(final String p) { + assert Thread.holdsLock(this); + try { + String name = fromLogManager(p.concat(".filter")); + if (hasValue(name)) { + filter = LogManagerProperties.newFilter(name); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if argument is null. + * @throws SecurityException if not allowed. + */ + private void initCapacity(final String p) { + assert Thread.holdsLock(this); + final int DEFAULT_CAPACITY = 1000; + try { + final String value = fromLogManager(p.concat(".capacity")); + if (value != null) { + this.setCapacity0(Integer.parseInt(value)); + } else { + this.setCapacity0(DEFAULT_CAPACITY); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final RuntimeException RE) { + reportError(RE.getMessage(), RE, ErrorManager.OPEN_FAILURE); + } + + if (capacity <= 0) { + capacity = DEFAULT_CAPACITY; + } + + this.data = new LogRecord[1]; + this.matched = new int[this.data.length]; + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initEncoding(final String p) { + assert Thread.holdsLock(this); + try { + String e = fromLogManager(p.concat(".encoding")); + if (e != null) { + setEncoding0(e); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (UnsupportedEncodingException | RuntimeException UEE) { + reportError(UEE.getMessage(), UEE, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Used to get or create the default ErrorManager used before init. + * @return the super error manager or a new ErrorManager. + * @since JavaMail 1.5.3 + */ + private ErrorManager defaultErrorManager() { + ErrorManager em; + try { //Try to share the super error manager. + em = super.getErrorManager(); + } catch (RuntimeException | LinkageError ignore) { + em = null; + } + + //Don't assume that the super call is not null. + if (em == null) { + em = new ErrorManager(); + } + return em; + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initErrorManager(final String p) { + assert Thread.holdsLock(this); + try { + String name = fromLogManager(p.concat(".errorManager")); + if (name != null) { + setErrorManager0(LogManagerProperties.newErrorManager(name)); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initFormatter(final String p) { + assert Thread.holdsLock(this); + try { + String name = fromLogManager(p.concat(".formatter")); + if (hasValue(name)) { + final Formatter f + = LogManagerProperties.newFormatter(name); + assert f != null; + if (f instanceof TailNameFormatter == false) { + formatter = f; + } else { + formatter = createSimpleFormatter(); + } + } else { + formatter = createSimpleFormatter(); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + formatter = createSimpleFormatter(); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initComparator(final String p) { + assert Thread.holdsLock(this); + try { + String name = fromLogManager(p.concat(".comparator")); + String reverse = fromLogManager(p.concat(".comparator.reverse")); + if (hasValue(name)) { + comparator = LogManagerProperties.newComparator(name); + if (Boolean.parseBoolean(reverse)) { + assert comparator != null : "null"; + comparator = LogManagerProperties.reverseOrder(comparator); + } + } else { + if (!isEmpty(reverse)) { + throw new IllegalArgumentException( + "No comparator to reverse."); + } + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initPushLevel(final String p) { + assert Thread.holdsLock(this); + try { + final String val = fromLogManager(p.concat(".pushLevel")); + if (val != null) { + this.pushLevel = Level.parse(val); + } + } catch (final RuntimeException RE) { + reportError(RE.getMessage(), RE, ErrorManager.OPEN_FAILURE); + } + + if (this.pushLevel == null) { + this.pushLevel = Level.OFF; + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initPushFilter(final String p) { + assert Thread.holdsLock(this); + try { + String name = fromLogManager(p.concat(".pushFilter")); + if (hasValue(name)) { + this.pushFilter = LogManagerProperties.newFilter(name); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initSubject(final String p) { + assert Thread.holdsLock(this); + String name = fromLogManager(p.concat(".subject")); + if (name == null) { //Soft dependency on CollectorFormatter. + name = "com.sun.mail.util.logging.CollectorFormatter"; + } + + if (hasValue(name)) { + try { + this.subjectFormatter = LogManagerProperties.newFormatter(name); + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (ClassNotFoundException + | ClassCastException literalSubject) { + this.subjectFormatter = TailNameFormatter.of(name); + } catch (final Exception E) { + this.subjectFormatter = TailNameFormatter.of(name); + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } else { //User has forced empty or literal null. + this.subjectFormatter = TailNameFormatter.of(name); + } + } + + /** + * Check if any attachment would actually format the given + * LogRecord. This method does not check if the handler + * is level is set to OFF or if the handler is closed. + * @param record a LogRecord + * @return true if the LogRecord would be formatted. + */ + private boolean isAttachmentLoggable(final LogRecord record) { + final Filter[] filters = readOnlyAttachmentFilters(); + for (int i = 0; i < filters.length; ++i) { + final Filter f = filters[i]; + if (f == null || f.isLoggable(record)) { + setMatchedPart(i); + return true; + } + } + return false; + } + + /** + * Check if this Handler would push after storing the + * LogRecord into its internal buffer. + * @param record a LogRecord + * @return true if the LogRecord triggers an email push. + * @throws NullPointerException if tryMutex was not called. + */ + private boolean isPushable(final LogRecord record) { + assert Thread.holdsLock(this); + final int value = getPushLevel().intValue(); + if (value == offValue || record.getLevel().intValue() < value) { + return false; + } + + final Filter push = getPushFilter(); + if (push == null) { + return true; + } + + final int match = getMatchedPart(); + if ((match == -1 && getFilter() == push) + || (match >= 0 && attachmentFilters[match] == push)) { + return true; + } else { + return push.isLoggable(record); + } + } + + /** + * Used to perform push or flush. + * @param priority true for high priority otherwise false for normal. + * @param code the error manager code. + */ + private void push(final boolean priority, final int code) { + if (tryMutex()) { + try { + final Message msg = writeLogRecords(code); + if (msg != null) { + send(msg, priority, code); + } + } catch (final LinkageError JDK8152515) { + reportLinkageError(JDK8152515, code); + } finally { + releaseMutex(); + } + } else { + reportUnPublishedError(null); + } + } + + /** + * Used to send the generated email or write its contents to the + * error manager for this handler. This method does not hold any + * locks so new records can be added to this handler during a send or + * failure. + * @param msg the message or null. + * @param priority true for high priority or false for normal. + * @param code the ErrorManager code. + * @throws NullPointerException if message is null. + */ + private void send(Message msg, boolean priority, int code) { + try { + envelopeFor(msg, priority); + final Object ccl = getAndSetContextClassLoader(MAILHANDLER_LOADER); + try { //JDK-8025251 + Transport.send(msg); //Calls save changes. + } finally { + getAndSetContextClassLoader(ccl); + } + } catch (final RuntimeException re) { + reportError(msg, re, code); + } catch (final Exception e) { + reportError(msg, e, code); + } + } + + /** + * Performs a sort on the records if needed. + * Any exception thrown during a sort is considered a formatting error. + */ + private void sort() { + assert Thread.holdsLock(this); + if (comparator != null) { + try { + if (size != 1) { + Arrays.sort(data, 0, size, comparator); + } else { + if (comparator.compare(data[0], data[0]) != 0) { + throw new IllegalArgumentException( + comparator.getClass().getName()); + } + } + } catch (final RuntimeException RE) { + reportError(RE.getMessage(), RE, ErrorManager.FORMAT_FAILURE); + } + } + } + + /** + * Formats all records in the buffer and places the output in a Message. + * This method under most conditions will catch, report, and continue when + * exceptions occur. This method holds a lock on this handler. + * @param code the error manager code. + * @return null if there are no records or is currently in a push. + * Otherwise a new message is created with a formatted message and + * attached session. + */ + private Message writeLogRecords(final int code) { + try { + synchronized (this) { + if (size > 0 && !isWriting) { + isWriting = true; + try { + return writeLogRecords0(); + } finally { + isWriting = false; + if (size > 0) { + reset(); + } + } + } + } + } catch (final RuntimeException re) { + reportError(re.getMessage(), re, code); + } catch (final Exception e) { + reportError(e.getMessage(), e, code); + } + return null; + } + + /** + * Formats all records in the buffer and places the output in a Message. + * This method under most conditions will catch, report, and continue when + * exceptions occur. + * + * @return null if there are no records or is currently in a push. Otherwise + * a new message is created with a formatted message and attached session. + * @throws MessagingException if there is a problem. + * @throws IOException if there is a problem. + * @throws RuntimeException if there is an unexpected problem. + * @since JavaMail 1.5.3 + */ + private Message writeLogRecords0() throws Exception { + assert Thread.holdsLock(this); + sort(); + if (session == null) { + initSession(); + } + MimeMessage msg = new MimeMessage(session); + + /** + * Parts are lazily created when an attachment performs a getHead + * call. Therefore, a null part at an index means that the head is + * required. + */ + MimeBodyPart[] parts = new MimeBodyPart[attachmentFormatters.length]; + + /** + * The buffers are lazily created when the part requires a getHead. + */ + StringBuilder[] buffers = new StringBuilder[parts.length]; + StringBuilder buf = null; + final MimePart body; + if (parts.length == 0) { + msg.setDescription(descriptionFrom( + getFormatter(), getFilter(), subjectFormatter)); + body = msg; + } else { + msg.setDescription(descriptionFrom( + comparator, pushLevel, pushFilter)); + body = createBodyPart(); + } + + appendSubject(msg, head(subjectFormatter)); + final Formatter bodyFormat = getFormatter(); + final Filter bodyFilter = getFilter(); + + Locale lastLocale = null; + for (int ix = 0; ix < size; ++ix) { + boolean formatted = false; + final int match = matched[ix]; + final LogRecord r = data[ix]; + data[ix] = null; //Clear while formatting. + + final Locale locale = localeFor(r); + appendSubject(msg, format(subjectFormatter, r)); + Filter lmf = null; //Identity of last matched filter. + if (bodyFilter == null || match == -1 || parts.length == 0 + || (match < -1 && bodyFilter.isLoggable(r))) { + lmf = bodyFilter; + if (buf == null) { + buf = new StringBuilder(); + buf.append(head(bodyFormat)); + } + formatted = true; + buf.append(format(bodyFormat, r)); + if (locale != null && !locale.equals(lastLocale)) { + appendContentLang(body, locale); + } + } + + for (int i = 0; i < parts.length; ++i) { + //A match index less than the attachment index means that + //the filter has not seen this record. + final Filter af = attachmentFilters[i]; + if (af == null || lmf == af || match == i + || (match < i && af.isLoggable(r))) { + if (lmf == null && af != null) { + lmf = af; + } + if (parts[i] == null) { + parts[i] = createBodyPart(i); + buffers[i] = new StringBuilder(); + buffers[i].append(head(attachmentFormatters[i])); + appendFileName(parts[i], head(attachmentNames[i])); + } + formatted = true; + appendFileName(parts[i], format(attachmentNames[i], r)); + buffers[i].append(format(attachmentFormatters[i], r)); + if (locale != null && !locale.equals(lastLocale)) { + appendContentLang(parts[i], locale); + } + } + } + + if (formatted) { + if (body != msg && locale != null + && !locale.equals(lastLocale)) { + appendContentLang(msg, locale); + } + } else { //Belongs to no mime part. + reportFilterError(r); + } + lastLocale = locale; + } + this.size = 0; + + for (int i = parts.length - 1; i >= 0; --i) { + if (parts[i] != null) { + appendFileName(parts[i], tail(attachmentNames[i], "err")); + buffers[i].append(tail(attachmentFormatters[i], "")); + + if (buffers[i].length() > 0) { + String name = parts[i].getFileName(); + if (isEmpty(name)) { //Exceptional case. + name = toString(attachmentFormatters[i]); + parts[i].setFileName(name); + } + setContent(parts[i], buffers[i], getContentType(name)); + } else { + setIncompleteCopy(msg); + parts[i] = null; //Skip this part. + } + buffers[i] = null; + } + } + + if (buf != null) { + buf.append(tail(bodyFormat, "")); + //This body part is always added, even if the buffer is empty, + //so the body is never considered an incomplete-copy. + } else { + buf = new StringBuilder(0); + } + + appendSubject(msg, tail(subjectFormatter, "")); + + String contentType = contentTypeOf(buf); + String altType = contentTypeOf(bodyFormat); + setContent(body, buf, altType == null ? contentType : altType); + if (body != msg) { + final MimeMultipart multipart = new MimeMultipart(); + //assert body instanceof BodyPart : body; + multipart.addBodyPart((BodyPart) body); + + for (int i = 0; i < parts.length; ++i) { + if (parts[i] != null) { + multipart.addBodyPart(parts[i]); + } + } + msg.setContent(multipart); + } + + return msg; + } + + /** + * Checks all of the settings if the caller requests a verify and a verify + * was not performed yet and no verify is in progress. A verify is + * performed on create because this handler may be at the end of a handler + * chain and therefore may not see any log records until LogManager.reset() + * is called and at that time all of the settings have been cleared. + * @param session the current session or null. + * @since JavaMail 1.4.4 + */ + private void verifySettings(final Session session) { + try { + if (session != null) { + final Properties props = session.getProperties(); + final Object check = props.put("verify", ""); + if (check instanceof String) { + String value = (String) check; + //Perform the verify if needed. + if (hasValue(value)) { + verifySettings0(session, value); + } + } else { + if (check != null) { //Pass some invalid string. + verifySettings0(session, check.getClass().toString()); + } + } + } + } catch (final LinkageError JDK8152515) { + reportLinkageError(JDK8152515, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Checks all of the settings using the given setting. + * This triggers the LogManagerProperties to copy all of the mail + * settings without explictly knowing them. Once all of the properties + * are copied this handler can handle LogManager.reset clearing all of the + * properties. It is expected that this method is, at most, only called + * once per session. + * @param session the current session. + * @param verify the type of verify to perform. + * @since JavaMail 1.4.4 + */ + private void verifySettings0(Session session, String verify) { + assert verify != null : (String) null; + if (!"local".equals(verify) && !"remote".equals(verify) + && !"limited".equals(verify) && !"resolve".equals(verify) + && !"login".equals(verify)) { + reportError("Verify must be 'limited', local', " + + "'resolve', 'login', or 'remote'.", + new IllegalArgumentException(verify), + ErrorManager.OPEN_FAILURE); + return; + } + + final MimeMessage abort = new MimeMessage(session); + final String msg; + if (!"limited".equals(verify)) { + msg = "Local address is " + + InternetAddress.getLocalAddress(session) + '.'; + + try { //Verify subclass or declared mime charset. + Charset.forName(getEncodingName()); + } catch (final RuntimeException RE) { + UnsupportedEncodingException UEE = + new UnsupportedEncodingException(RE.toString()); + UEE.initCause(RE); + reportError(msg, UEE, ErrorManager.FORMAT_FAILURE); + } + } else { + msg = "Skipping local address check."; + } + + //Perform all of the copy actions first. + String[] atn; + synchronized (this) { //Create the subject. + appendSubject(abort, head(subjectFormatter)); + appendSubject(abort, tail(subjectFormatter, "")); + atn = new String[attachmentNames.length]; + for (int i = 0; i < atn.length; ++i) { + atn[i] = head(attachmentNames[i]); + if (atn[i].length() == 0) { + atn[i] = tail(attachmentNames[i], ""); + } else { + atn[i] = atn[i].concat(tail(attachmentNames[i], "")); + } + } + } + + setIncompleteCopy(abort); //Original body part is never added. + envelopeFor(abort, true); + saveChangesNoContent(abort, msg); + try { + //Ensure transport provider is installed. + Address[] all = abort.getAllRecipients(); + if (all == null) { //Don't pass null to sendMessage. + all = new InternetAddress[0]; + } + Transport t; + try { + final Address[] any = all.length != 0 ? all : abort.getFrom(); + if (any != null && any.length != 0) { + t = session.getTransport(any[0]); + session.getProperty("mail.transport.protocol"); //Force copy + } else { + MessagingException me = new MessagingException( + "No recipient or from address."); + reportError(msg, me, ErrorManager.OPEN_FAILURE); + throw me; + } + } catch (final MessagingException protocol) { + //Switching the CCL emulates the current send behavior. + Object ccl = getAndSetContextClassLoader(MAILHANDLER_LOADER); + try { + t = session.getTransport(); + } catch (final MessagingException fail) { + throw attach(protocol, fail); + } finally { + getAndSetContextClassLoader(ccl); + } + } + + String local = null; + if ("remote".equals(verify) || "login".equals(verify)) { + MessagingException closed = null; + t.connect(); + try { + try { + //Capture localhost while connection is open. + local = getLocalHost(t); + + //A message without content will fail at message writeTo + //when sendMessage is called. This allows the handler + //to capture all mail properties set in the LogManager. + if ("remote".equals(verify)) { + t.sendMessage(abort, all); + } + } finally { + try { + t.close(); + } catch (final MessagingException ME) { + closed = ME; + } + } + //Close the transport before reporting errors. + if ("remote".equals(verify)) { + reportUnexpectedSend(abort, verify, null); + } else { + final String protocol = t.getURLName().getProtocol(); + verifyProperties(session, protocol); + } + } catch (final SendFailedException sfe) { + Address[] recip = sfe.getInvalidAddresses(); + if (recip != null && recip.length != 0) { + setErrorContent(abort, verify, sfe); + reportError(abort, sfe, ErrorManager.OPEN_FAILURE); + } + + recip = sfe.getValidSentAddresses(); + if (recip != null && recip.length != 0) { + reportUnexpectedSend(abort, verify, sfe); + } + } catch (final MessagingException ME) { + if (!isMissingContent(abort, ME)) { + setErrorContent(abort, verify, ME); + reportError(abort, ME, ErrorManager.OPEN_FAILURE); + } + } + + if (closed != null) { + setErrorContent(abort, verify, closed); + reportError(abort, closed, ErrorManager.CLOSE_FAILURE); + } + } else { + //Force a property copy, JDK-7092981. + final String protocol = t.getURLName().getProtocol(); + verifyProperties(session, protocol); + String mailHost = session.getProperty("mail." + + protocol + ".host"); + if (isEmpty(mailHost)) { + mailHost = session.getProperty("mail.host"); + } else { + session.getProperty("mail.host"); + } + + local = session.getProperty("mail." + protocol + ".localhost"); + if (isEmpty(local)) { + local = session.getProperty("mail." + + protocol + ".localaddress"); + } else { + session.getProperty("mail." + protocol + ".localaddress"); + } + + if ("resolve".equals(verify)) { + try { //Resolve the remote host name. + String transportHost = t.getURLName().getHost(); + if (!isEmpty(transportHost)) { + verifyHost(transportHost); + if (!transportHost.equalsIgnoreCase(mailHost)) { + verifyHost(mailHost); + } + } else { + verifyHost(mailHost); + } + } catch (final RuntimeException | IOException IOE) { + MessagingException ME = + new MessagingException(msg, IOE); + setErrorContent(abort, verify, ME); + reportError(abort, ME, ErrorManager.OPEN_FAILURE); + } + } + } + + if (!"limited".equals(verify)) { + try { //Verify host name and hit the host name cache. + if (!"remote".equals(verify) && !"login".equals(verify)) { + local = getLocalHost(t); + } + verifyHost(local); + } catch (final RuntimeException | IOException IOE) { + MessagingException ME = new MessagingException(msg, IOE); + setErrorContent(abort, verify, ME); + reportError(abort, ME, ErrorManager.OPEN_FAILURE); + } + + try { //Verify that the DataHandler can be loaded. + Object ccl = getAndSetContextClassLoader(MAILHANDLER_LOADER); + try { + //Always load the multipart classes. + MimeMultipart multipart = new MimeMultipart(); + MimeBodyPart[] ambp = new MimeBodyPart[atn.length]; + final MimeBodyPart body; + final String bodyContentType; + synchronized (this) { + bodyContentType = contentTypeOf(getFormatter()); + body = createBodyPart(); + for (int i = 0; i < atn.length; ++i) { + ambp[i] = createBodyPart(i); + ambp[i].setFileName(atn[i]); + //Convert names to mime type under lock. + atn[i] = getContentType(atn[i]); + } + } + + body.setDescription(verify); + setContent(body, "", bodyContentType); + multipart.addBodyPart(body); + for (int i = 0; i < ambp.length; ++i) { + ambp[i].setDescription(verify); + setContent(ambp[i], "", atn[i]); + } + + abort.setContent(multipart); + abort.saveChanges(); + abort.writeTo(new ByteArrayOutputStream(MIN_HEADER_SIZE)); + } finally { + getAndSetContextClassLoader(ccl); + } + } catch (final IOException IOE) { + MessagingException ME = new MessagingException(msg, IOE); + setErrorContent(abort, verify, ME); + reportError(abort, ME, ErrorManager.FORMAT_FAILURE); + } + } + + //Verify all recipients. + if (all.length != 0) { + verifyAddresses(all); + } else { + throw new MessagingException("No recipient addresses."); + } + + //Verify from and sender addresses. + Address[] from = abort.getFrom(); + Address sender = abort.getSender(); + if (sender instanceof InternetAddress) { + ((InternetAddress) sender).validate(); + } + + //If from address is declared then check sender. + if (abort.getHeader("From", ",") != null && from.length != 0) { + verifyAddresses(from); + for (int i = 0; i < from.length; ++i) { + if (from[i].equals(sender)) { + MessagingException ME = new MessagingException( + "Sender address '" + sender + + "' equals from address."); + throw new MessagingException(msg, ME); + } + } + } else { + if (sender == null) { + MessagingException ME = new MessagingException( + "No from or sender address."); + throw new MessagingException(msg, ME); + } + } + + //Verify reply-to addresses. + verifyAddresses(abort.getReplyTo()); + } catch (final RuntimeException RE) { + setErrorContent(abort, verify, RE); + reportError(abort, RE, ErrorManager.OPEN_FAILURE); + } catch (final Exception ME) { + setErrorContent(abort, verify, ME); + reportError(abort, ME, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Handles all exceptions thrown when save changes is called on a message + * that doesn't have any content. + * + * @param abort the message requiring save changes. + * @param msg the error description. + * @since JavaMail 1.6.0 + */ + private void saveChangesNoContent(final Message abort, final String msg) { + if (abort != null) { + try { + try { + abort.saveChanges(); + } catch (final NullPointerException xferEncoding) { + //Workaround GNU JavaMail bug in MimeUtility.getEncoding + //when the mime message has no content. + try { + String cte = "Content-Transfer-Encoding"; + if (abort.getHeader(cte) == null) { + abort.setHeader(cte, "base64"); + abort.saveChanges(); + } else { + throw xferEncoding; + } + } catch (RuntimeException | MessagingException e) { + if (e != xferEncoding) { + e.addSuppressed(xferEncoding); + } + throw e; + } + } + } catch (RuntimeException | MessagingException ME) { + reportError(msg, ME, ErrorManager.FORMAT_FAILURE); + } + } + } + + /** + * Cache common session properties into the LogManagerProperties. This is + * a workaround for JDK-7092981. + * + * @param session the session. + * @param protocol the mail protocol. + * @throws NullPointerException if session is null. + * @since JavaMail 1.6.0 + */ + private static void verifyProperties(Session session, String protocol) { + session.getProperty("mail.from"); + session.getProperty("mail." + protocol + ".from"); + session.getProperty("mail.dsn.ret"); + session.getProperty("mail." + protocol + ".dsn.ret"); + session.getProperty("mail.dsn.notify"); + session.getProperty("mail." + protocol + ".dsn.notify"); + session.getProperty("mail." + protocol + ".port"); + session.getProperty("mail.user"); + session.getProperty("mail." + protocol + ".user"); + session.getProperty("mail." + protocol + ".localport"); + } + + /** + * Perform a lookup of the host address or FQDN. + * @param host the host or null. + * @return the address. + * @throws IOException if the host name is not valid. + * @throws SecurityException if security manager is present and doesn't + * allow access to check connect permission. + * @since JavaMail 1.5.0 + */ + private static InetAddress verifyHost(String host) throws IOException { + InetAddress a; + if (isEmpty(host)) { + a = InetAddress.getLocalHost(); + } else { + a = InetAddress.getByName(host); + } + if (a.getCanonicalHostName().length() == 0) { + throw new UnknownHostException(); + } + return a; + } + + /** + * Calls validate for every address given. + * If the addresses given are null, empty or not an InternetAddress then + * the check is skipped. + * @param all any address array, null or empty. + * @throws AddressException if there is a problem. + * @since JavaMail 1.4.5 + */ + private static void verifyAddresses(Address[] all) throws AddressException { + if (all != null) { + for (int i = 0; i < all.length; ++i) { + final Address a = all[i]; + if (a instanceof InternetAddress) { + ((InternetAddress) a).validate(); + } + } + } + } + + /** + * Reports that an empty content message was sent and should not have been. + * @param msg the MimeMessage. + * @param verify the verify enum. + * @param cause the exception that caused the problem or null. + * @since JavaMail 1.4.5 + */ + private void reportUnexpectedSend(MimeMessage msg, String verify, Exception cause) { + final MessagingException write = new MessagingException( + "An empty message was sent.", cause); + setErrorContent(msg, verify, write); + reportError(msg, write, ErrorManager.OPEN_FAILURE); + } + + /** + * Creates and sets the message content from the given Throwable. + * When verify fails, this method fixes the 'abort' message so that any + * created envelope data can be used in the error manager. + * @param msg the message with or without content. + * @param verify the verify enum. + * @param t the throwable or null. + * @since JavaMail 1.4.5 + */ + private void setErrorContent(MimeMessage msg, String verify, Throwable t) { + try { //Add content so toRawString doesn't fail. + final MimeBodyPart body; + final String subjectType; + final String msgDesc; + synchronized (this) { + body = createBodyPart(); + msgDesc = descriptionFrom(comparator, pushLevel, pushFilter); + subjectType = getClassId(subjectFormatter); + } + + body.setDescription("Formatted using " + + (t == null ? Throwable.class.getName() + : t.getClass().getName()) + ", filtered with " + + verify + ", and named by " + + subjectType + '.'); + setContent(body, toMsgString(t), "text/plain"); + final MimeMultipart multipart = new MimeMultipart(); + multipart.addBodyPart(body); + msg.setContent(multipart); + msg.setDescription(msgDesc); + setAcceptLang(msg); + msg.saveChanges(); + } catch (MessagingException | RuntimeException ME) { + reportError("Unable to create body.", ME, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Used to update the cached session object based on changes in + * mail properties or authenticator. + * @return the current session or null if no verify is required. + */ + private Session updateSession() { + assert Thread.holdsLock(this); + final Session settings; + if (mailProps.getProperty("verify") != null) { + settings = initSession(); + assert settings == session : session; + } else { + session = null; //Remove old session. + settings = null; + } + return settings; + } + + /** + * Creates a session using a proxy properties object. + * @return the session that was created and assigned. + */ + private Session initSession() { + assert Thread.holdsLock(this); + final String p = getClass().getName(); + LogManagerProperties proxy = new LogManagerProperties(mailProps, p); + session = Session.getInstance(proxy, auth); + return session; + } + + /** + * Creates all of the envelope information for a message. + * This method is safe to call outside of a lock because the message + * provides the safe snapshot of the mail properties. + * @param msg the Message to write the envelope information. + * @param priority true for high priority. + */ + private void envelopeFor(Message msg, boolean priority) { + setAcceptLang(msg); + setFrom(msg); + if (!setRecipient(msg, "mail.to", Message.RecipientType.TO)) { + setDefaultRecipient(msg, Message.RecipientType.TO); + } + setRecipient(msg, "mail.cc", Message.RecipientType.CC); + setRecipient(msg, "mail.bcc", Message.RecipientType.BCC); + setReplyTo(msg); + setSender(msg); + setMailer(msg); + setAutoSubmitted(msg); + if (priority) { + setPriority(msg); + } + + try { + msg.setSentDate(new java.util.Date()); + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Factory to create the in-line body part. + * @return a body part with default headers set. + * @throws MessagingException if there is a problem. + */ + private MimeBodyPart createBodyPart() throws MessagingException { + assert Thread.holdsLock(this); + final MimeBodyPart part = new MimeBodyPart(); + part.setDisposition(Part.INLINE); + part.setDescription(descriptionFrom(getFormatter(), + getFilter(), subjectFormatter)); + setAcceptLang(part); + return part; + } + + /** + * Factory to create the attachment body part. + * @param index the attachment index. + * @return a body part with default headers set. + * @throws MessagingException if there is a problem. + * @throws IndexOutOfBoundsException if the given index is not an valid + * attachment index. + */ + private MimeBodyPart createBodyPart(int index) throws MessagingException { + assert Thread.holdsLock(this); + final MimeBodyPart part = new MimeBodyPart(); + part.setDisposition(Part.ATTACHMENT); + part.setDescription(descriptionFrom( + attachmentFormatters[index], + attachmentFilters[index], + attachmentNames[index])); + setAcceptLang(part); + return part; + } + + /** + * Gets the description for the MimeMessage itself. + * The push level and filter are included because they play a role in + * formatting of a message when triggered or not triggered. + * @param c the comparator. + * @param l the pushLevel. + * @param f the pushFilter + * @return the description. + * @throws NullPointerException if level is null. + * @since JavaMail 1.4.5 + */ + private String descriptionFrom(Comparator c, Level l, Filter f) { + return "Sorted using "+ (c == null ? "no comparator" + : c.getClass().getName()) + ", pushed when "+ l.getName() + + ", and " + (f == null ? "no push filter" + : f.getClass().getName()) + '.'; + } + + /** + * Creates a description for a body part. + * @param f the content formatter. + * @param filter the content filter. + * @param name the naming formatter. + * @return the description for the body part. + */ + private String descriptionFrom(Formatter f, Filter filter, Formatter name) { + return "Formatted using " + getClassId(f) + + ", filtered with " + (filter == null ? "no filter" + : filter.getClass().getName()) +", and named by " + + getClassId(name) + '.'; + } + + /** + * Gets a class name represents the behavior of the formatter. + * The class name may not be assignable to a Formatter. + * @param f the formatter. + * @return a class name that represents the given formatter. + * @throws NullPointerException if the parameter is null. + * @since JavaMail 1.4.5 + */ + private String getClassId(final Formatter f) { + if (f instanceof TailNameFormatter) { + return String.class.getName(); //Literal string. + } else { + return f.getClass().getName(); + } + } + + /** + * Ensure that a formatter creates a valid string for a part name. + * @param f the formatter. + * @return the to string value or the class name. + */ + private String toString(final Formatter f) { + //Should never be null but, guard against formatter bugs. + final String name = f.toString(); + if (!isEmpty(name)) { + return name; + } else { + return getClassId(f); + } + } + + /** + * Constructs a file name from a formatter. This method is called often + * but, rarely does any work. + * @param part to append to. + * @param chunk non null string to append. + */ + private void appendFileName(final Part part, final String chunk) { + if (chunk != null) { + if (chunk.length() > 0) { + appendFileName0(part, chunk); + } + } else { + reportNullError(ErrorManager.FORMAT_FAILURE); + } + } + + /** + * It is assumed that file names are short and that in most cases + * getTail will be the only method that will produce a result. + * @param part to append to. + * @param chunk non null string to append. + */ + private void appendFileName0(final Part part, String chunk) { + try { + //Remove all control character groups. + chunk = chunk.replaceAll("[\\x00-\\x1F\\x7F]+", ""); + final String old = part.getFileName(); + part.setFileName(old != null ? old.concat(chunk) : chunk); + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Constructs a subject line from a formatter. + * @param msg to append to. + * @param chunk non null string to append. + */ + private void appendSubject(final Message msg, final String chunk) { + if (chunk != null) { + if (chunk.length() > 0) { + appendSubject0(msg, chunk); + } + } else { + reportNullError(ErrorManager.FORMAT_FAILURE); + } + } + + /** + * It is assumed that subject lines are short and that in most cases + * getTail will be the only method that will produce a result. + * @param msg to append to. + * @param chunk non null string to append. + */ + private void appendSubject0(final Message msg, String chunk) { + try { + //Remove all control character groups. + chunk = chunk.replaceAll("[\\x00-\\x1F\\x7F]+", ""); + final String charset = getEncodingName(); + final String old = msg.getSubject(); + assert msg instanceof MimeMessage : msg; + ((MimeMessage) msg).setSubject(old != null ? old.concat(chunk) + : chunk, MimeUtility.mimeCharset(charset)); + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Gets the locale for the given log record from the resource bundle. + * If the resource bundle is using the root locale then the default locale + * is returned. + * @param r the log record. + * @return null if not localized otherwise, the locale of the record. + * @since JavaMail 1.4.5 + */ + private Locale localeFor(final LogRecord r) { + Locale l; + final ResourceBundle rb = r.getResourceBundle(); + if (rb != null) { + l = rb.getLocale(); + if (l == null || isEmpty(l.getLanguage())) { + //The language of the fallback bundle (root) is unknown. + //1. Use default locale. Should only be wrong if the app is + // used with a langauge that was unintended. (unlikely) + //2. Mark it as not localized (force null, info loss). + //3. Use the bundle name (encoded) as an experimental language. + l = Locale.getDefault(); + } + } else { + l = null; + } + return l; + } + + /** + * Appends the content language to the given mime part. + * The language tag is only appended if the given language has not been + * specified. This method is only used when we have LogRecords that are + * localized with an assigned resource bundle. + * @param p the mime part. + * @param l the locale to append. + * @throws NullPointerException if any argument is null. + * @since JavaMail 1.4.5 + */ + private void appendContentLang(final MimePart p, final Locale l) { + try { + String lang = LogManagerProperties.toLanguageTag(l); + if (lang.length() != 0) { + String header = p.getHeader("Content-Language", null); + if (isEmpty(header)) { + p.setHeader("Content-Language", lang); + } else if (!header.equalsIgnoreCase(lang)) { + lang = ",".concat(lang); + int idx = 0; + while ((idx = header.indexOf(lang, idx)) > -1) { + idx += lang.length(); + if (idx == header.length() + || header.charAt(idx) == ',') { + break; + } + } + + if (idx < 0) { + int len = header.lastIndexOf("\r\n\t"); + if (len < 0) { //If not folded. + len = (18 + 2) + header.length(); + } else { + len = (header.length() - len) + 8; + } + + //Perform folding of header if needed. + if ((len + lang.length()) > 76) { + header = header.concat("\r\n\t".concat(lang)); + } else { + header = header.concat(lang); + } + p.setHeader("Content-Language", header); + } + } + } + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Sets the accept language to the default locale of the JVM. + * If the locale is the root locale the header is not added. + * @param p the part to set. + * @since JavaMail 1.4.5 + */ + private void setAcceptLang(final Part p) { + try { + final String lang = LogManagerProperties + .toLanguageTag(Locale.getDefault()); + if (lang.length() != 0) { + p.setHeader("Accept-Language", lang); + } + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Used when a log record was loggable prior to being inserted + * into the buffer but at the time of formatting was no longer loggable. + * Filters were changed after publish but prior to a push or a bug in the + * body filter or one of the attachment filters. + * @param record that was not formatted. + * @since JavaMail 1.4.5 + */ + private void reportFilterError(final LogRecord record) { + assert Thread.holdsLock(this); + final Formatter f = createSimpleFormatter(); + final String msg = "Log record " + record.getSequenceNumber() + + " was filtered from all message parts. " + + head(f) + format(f, record) + tail(f, ""); + final String txt = getFilter() + ", " + + Arrays.asList(readOnlyAttachmentFilters()); + reportError(msg, new IllegalArgumentException(txt), + ErrorManager.FORMAT_FAILURE); + } + + /** + * Reports symmetric contract violations an equals implementation. + * @param o the test object must be non null. + * @param found the possible intern, must be non null. + * @throws NullPointerException if any argument is null. + * @since JavaMail 1.5.0 + */ + private void reportNonSymmetric(final Object o, final Object found) { + reportError("Non symmetric equals implementation." + , new IllegalArgumentException(o.getClass().getName() + + " is not equal to " + found.getClass().getName()) + , ErrorManager.OPEN_FAILURE); + } + + /** + * Reports equals implementations that do not discriminate between objects + * of different types or subclass types. + * @param o the test object must be non null. + * @param found the possible intern, must be non null. + * @throws NullPointerException if any argument is null. + * @since JavaMail 1.5.0 + */ + private void reportNonDiscriminating(final Object o, final Object found) { + reportError("Non discriminating equals implementation." + , new IllegalArgumentException(o.getClass().getName() + + " should not be equal to " + found.getClass().getName()) + , ErrorManager.OPEN_FAILURE); + } + + /** + * Used to outline the bytes to report a null pointer exception. + * See BUD ID 6533165. + * @param code the ErrorManager code. + */ + private void reportNullError(final int code) { + reportError("null", new NullPointerException(), code); + } + + /** + * Creates the head or reports a formatting error. + * @param f the formatter. + * @return the head string or an empty string. + */ + private String head(final Formatter f) { + try { + return f.getHead(this); + } catch (final RuntimeException RE) { + reportError(RE.getMessage(), RE, ErrorManager.FORMAT_FAILURE); + return ""; + } + } + + /** + * Creates the formatted log record or reports a formatting error. + * @param f the formatter. + * @param r the log record. + * @return the formatted string or an empty string. + */ + private String format(final Formatter f, final LogRecord r) { + try { + return f.format(r); + } catch (final RuntimeException RE) { + reportError(RE.getMessage(), RE, ErrorManager.FORMAT_FAILURE); + return ""; + } + } + + /** + * Creates the tail or reports a formatting error. + * @param f the formatter. + * @param def the default string to use when there is an error. + * @return the tail string or the given default string. + */ + private String tail(final Formatter f, final String def) { + try { + return f.getTail(this); + } catch (final RuntimeException RE) { + reportError(RE.getMessage(), RE, ErrorManager.FORMAT_FAILURE); + return def; + } + } + + /** + * Sets the x-mailer header. + * @param msg the target message. + */ + private void setMailer(final Message msg) { + try { + final Class mail = MailHandler.class; + final Class k = getClass(); + String value; + if (k == mail) { + value = mail.getName(); + } else { + try { + value = MimeUtility.encodeText(k.getName()); + } catch (final UnsupportedEncodingException E) { + reportError(E.getMessage(), E, ErrorManager.FORMAT_FAILURE); + value = k.getName().replaceAll("[^\\x00-\\x7F]", "\uu001A"); + } + value = MimeUtility.fold(10, mail.getName() + " using the " + + value + " extension."); + } + msg.setHeader("X-Mailer", value); + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Sets the priority and importance headers. + * @param msg the target message. + */ + private void setPriority(final Message msg) { + try { + msg.setHeader("Importance", "High"); + msg.setHeader("Priority", "urgent"); + msg.setHeader("X-Priority", "2"); //High + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Used to signal that body parts are missing from a message. Also used + * when LogRecords were passed to an attachment formatter but the formatter + * produced no output, which is allowed. Used during a verify because all + * parts are omitted, none of the content formatters are used. This is + * not used when a filter prevents LogRecords from being formatted. + * This header is defined in RFC 2156 and RFC 4021. + * @param msg the message. + * @since JavaMail 1.4.5 + */ + private void setIncompleteCopy(final Message msg) { + try { + msg.setHeader("Incomplete-Copy", ""); + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Signals that this message was generated by automatic process. + * This header is defined in RFC 3834 section 5. + * @param msg the message. + * @since JavaMail 1.4.6 + */ + private void setAutoSubmitted(final Message msg) { + if (allowRestrictedHeaders()) { + try { //RFC 3834 (5.2) + msg.setHeader("auto-submitted", "auto-generated"); + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + } + + /** + * Sets from address header. + * @param msg the target message. + */ + private void setFrom(final Message msg) { + final String from = getSession(msg).getProperty("mail.from"); + if (from != null) { + try { + final Address[] address = InternetAddress.parse(from, false); + if (address.length > 0) { + if (address.length == 1) { + msg.setFrom(address[0]); + } else { //Greater than 1 address. + msg.addFrom(address); + } + } + //Can't place an else statement here because the 'from' is + //not null which causes the local address computation + //to fail. Assume the user wants to omit the from address + //header. + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + setDefaultFrom(msg); + } + } else { + setDefaultFrom(msg); + } + } + + /** + * Sets the from header to the local address. + * @param msg the target message. + */ + private void setDefaultFrom(final Message msg) { + try { + msg.setFrom(); + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Computes the default to-address if none was specified. This can + * fail if the local address can't be computed. + * @param msg the message + * @param type the recipient type. + * @since JavaMail 1.5.0 + */ + private void setDefaultRecipient(final Message msg, + final Message.RecipientType type) { + try { + Address a = InternetAddress.getLocalAddress(getSession(msg)); + if (a != null) { + msg.setRecipient(type, a); + } else { + final MimeMessage m = new MimeMessage(getSession(msg)); + m.setFrom(); //Should throw an exception with a cause. + Address[] from = m.getFrom(); + if (from.length > 0) { + msg.setRecipients(type, from); + } else { + throw new MessagingException("No local address."); + } + } + } catch (MessagingException | RuntimeException ME) { + reportError("Unable to compute a default recipient.", + ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Sets reply-to address header. + * @param msg the target message. + */ + private void setReplyTo(final Message msg) { + final String reply = getSession(msg).getProperty("mail.reply.to"); + if (!isEmpty(reply)) { + try { + final Address[] address = InternetAddress.parse(reply, false); + if (address.length > 0) { + msg.setReplyTo(address); + } + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + } + + /** + * Sets sender address header. + * @param msg the target message. + */ + private void setSender(final Message msg) { + assert msg instanceof MimeMessage : msg; + final String sender = getSession(msg).getProperty("mail.sender"); + if (!isEmpty(sender)) { + try { + final InternetAddress[] address = + InternetAddress.parse(sender, false); + if (address.length > 0) { + ((MimeMessage) msg).setSender(address[0]); + if (address.length > 1) { + reportError("Ignoring other senders.", + tooManyAddresses(address, 1), + ErrorManager.FORMAT_FAILURE); + } + } + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + } + + /** + * A common factory used to create the too many addresses exception. + * @param address the addresses, never null. + * @param offset the starting address to display. + * @return the too many addresses exception. + */ + private AddressException tooManyAddresses(Address[] address, int offset) { + Object l = Arrays.asList(address).subList(offset, address.length); + return new AddressException(l.toString()); + } + + /** + * Sets the recipient for the given message. + * @param msg the message. + * @param key the key to search in the session. + * @param type the recipient type. + * @return true if the key was contained in the session. + */ + private boolean setRecipient(final Message msg, + final String key, final Message.RecipientType type) { + boolean containsKey; + final String value = getSession(msg).getProperty(key); + containsKey = value != null; + if (!isEmpty(value)) { + try { + final Address[] address = InternetAddress.parse(value, false); + if (address.length > 0) { + msg.setRecipients(type, address); + } + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + return containsKey; + } + + /** + * Converts an email message to a raw string. This raw string + * is passed to the error manager to allow custom error managers + * to recreate the original MimeMessage object. + * @param msg a Message object. + * @return the raw string or null if msg was null. + * @throws MessagingException if there was a problem with the message. + * @throws IOException if there was a problem. + */ + private String toRawString(final Message msg) throws MessagingException, IOException { + if (msg != null) { + Object ccl = getAndSetContextClassLoader(MAILHANDLER_LOADER); + try { //JDK-8025251 + int nbytes = Math.max(msg.getSize() + MIN_HEADER_SIZE, MIN_HEADER_SIZE); + ByteArrayOutputStream out = new ByteArrayOutputStream(nbytes); + msg.writeTo(out); //Headers can be UTF-8 or US-ASCII. + return out.toString("UTF-8"); + } finally { + getAndSetContextClassLoader(ccl); + } + } else { //Must match this.reportError behavior, see push method. + return null; //Null is the safe choice. + } + } + + /** + * Converts a throwable to a message string. + * @param t any throwable or null. + * @return the throwable with a stack trace or the literal null. + */ + private String toMsgString(final Throwable t) { + if (t == null) { + return "null"; + } + + final String charset = getEncodingName(); + try { + final ByteArrayOutputStream out = + new ByteArrayOutputStream(MIN_HEADER_SIZE); + + //Create an output stream writer so streams are not double buffered. + try (OutputStreamWriter ows = new OutputStreamWriter(out, charset); + PrintWriter pw = new PrintWriter(ows)) { + pw.println(t.getMessage()); + t.printStackTrace(pw); + pw.flush(); + } //Close OSW before generating string. JDK-6995537 + return out.toString(charset); + } catch (final RuntimeException unexpected) { + return t.toString() + ' ' + unexpected.toString(); + } catch (final Exception badMimeCharset) { + return t.toString() + ' ' + badMimeCharset.toString(); + } + } + + /** + * Replaces the current context class loader with our class loader. + * @param ccl null for boot class loader, a class loader, a class used to + * get the class loader, or a source object to get the class loader. + * @return null for the boot class loader, a class loader, or a marker + * object to signal that no modification was required. + * @since JavaMail 1.5.3 + */ + private Object getAndSetContextClassLoader(final Object ccl) { + if (ccl != GetAndSetContext.NOT_MODIFIED) { + try { + final PrivilegedAction pa; + if (ccl instanceof PrivilegedAction) { + pa = (PrivilegedAction) ccl; + } else { + pa = new GetAndSetContext(ccl); + } + return AccessController.doPrivileged(pa); + } catch (final SecurityException ignore) { + } + } + return GetAndSetContext.NOT_MODIFIED; + } + + /** + * A factory used to create a common attachment mismatch type. + * @param msg the exception message. + * @return a RuntimeException to represent the type of error. + */ + private static RuntimeException attachmentMismatch(final String msg) { + return new IndexOutOfBoundsException(msg); + } + + /** + * Outline the attachment mismatch message. See Bug ID 6533165. + * @param expected the expected array length. + * @param found the array length that was given. + * @return a RuntimeException populated with a message. + */ + private static RuntimeException attachmentMismatch(int expected, int found) { + return attachmentMismatch("Attachments mismatched, expected " + + expected + " but given " + found + '.'); + } + + /** + * Try to attach a suppressed exception to a MessagingException in any order + * that is possible. + * @param required the exception expected to see as a reported failure. + * @param optional the suppressed exception. + * @return either the required or the optional exception. + */ + private static MessagingException attach( + MessagingException required, Exception optional) { + if (optional != null && !required.setNextException(optional)) { + if (optional instanceof MessagingException) { + final MessagingException head = (MessagingException) optional; + if (head.setNextException(required)) { + return head; + } + } + + if (optional != required) { + required.addSuppressed(optional); + } + } + return required; + } + + /** + * Gets the local host from the given service object. + * @param s the service to check. + * @return the local host or null. + * @since JavaMail 1.5.3 + */ + private String getLocalHost(final Service s) { + try { + return LogManagerProperties.getLocalHost(s); + } catch (SecurityException | NoSuchMethodException + | LinkageError ignore) { + } catch (final Exception ex) { + reportError(s.toString(), ex, ErrorManager.OPEN_FAILURE); + } + return null; + } + + /** + * Google App Engine doesn't support Message.getSession. + * @param msg the message. + * @return the session from the given message. + * @throws NullPointerException if the given message is null. + * @since JavaMail 1.5.3 + */ + private Session getSession(final Message msg) { + if (msg == null) { + throw new NullPointerException(); + } + return new MessageContext(msg).getSession(); + } + + /** + * Determines if restricted headers are allowed in the current environment. + * + * @return true if restricted headers are allowed. + * @since JavaMail 1.5.3 + */ + private boolean allowRestrictedHeaders() { + //GAE will prevent delivery of email with forbidden headers. + //Assume the environment is GAE if access to the LogManager is + //forbidden. + return LogManagerProperties.hasLogManager(); + } + + /** + * Outline the creation of the index error message. See JDK-6533165. + * @param i the index. + * @return the error message. + */ + private static String atIndexMsg(final int i) { + return "At index: " + i + '.'; + } + + /** + * Used for storing a password from the LogManager or literal string. + * @since JavaMail 1.4.6 + */ + private static final class DefaultAuthenticator extends Authenticator { + + /** + * Creates an Authenticator for the given password. This method is used + * so class verification of assignments in MailHandler doesn't require + * loading this class which otherwise can occur when using the + * constructor. Default access to avoid generating extra class files. + * + * @param pass the password. + * @return an Authenticator for the password. + * @since JavaMail 1.5.6 + */ + static Authenticator of(final String pass) { + return new DefaultAuthenticator(pass); + } + + /** + * The password to use. + */ + private final String pass; + + /** + * Use the factory method instead of this constructor. + * @param pass the password. + */ + private DefaultAuthenticator(final String pass) { + assert pass != null; + this.pass = pass; + } + + @Override + protected final PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(getDefaultUserName(), pass); + } + } + + /** + * Performs a get and set of the context class loader with privileges + * enabled. + * @since JavaMail 1.4.6 + */ + private static final class GetAndSetContext implements PrivilegedAction { + /** + * A marker object used to signal that the class loader was not + * modified. + */ + public static final Object NOT_MODIFIED = GetAndSetContext.class; + /** + * The source containing the class loader. + */ + private final Object source; + /** + * Create the action. + * @param source null for boot class loader, a class loader, a class + * used to get the class loader, or a source object to get the class + * loader. Default access to avoid generating extra class files. + */ + GetAndSetContext(final Object source) { + this.source = source; + } + + /** + * Gets the class loader from the source and sets the CCL only if + * the source and CCL are not the same. + * @return the replaced context class loader which can be null or + * NOT_MODIFIED to indicate that nothing was modified. + */ + @SuppressWarnings("override") //JDK-6954234 + public final Object run() { + final Thread current = Thread.currentThread(); + final ClassLoader ccl = current.getContextClassLoader(); + final ClassLoader loader; + if (source == null) { + loader = null; //boot class loader + } else if (source instanceof ClassLoader) { + loader = (ClassLoader) source; + } else if (source instanceof Class) { + loader = ((Class) source).getClassLoader(); + } else if (source instanceof Thread) { + loader = ((Thread) source).getContextClassLoader(); + } else { + assert !(source instanceof Class) : source; + loader = source.getClass().getClassLoader(); + } + + if (ccl != loader) { + current.setContextClassLoader(loader); + return ccl; + } else { + return NOT_MODIFIED; + } + } + } + + /** + * Used for naming attachment file names and the main subject line. + */ + private static final class TailNameFormatter extends Formatter { + + /** + * Creates or gets a formatter from the given name. This method is used + * so class verification of assignments in MailHandler doesn't require + * loading this class which otherwise can occur when using the + * constructor. Default access to avoid generating extra class files. + * + * @param name any not null string. + * @return a formatter for that string. + * @since JavaMail 1.5.6 + */ + static Formatter of(final String name) { + return new TailNameFormatter(name); + } + + /** + * The value used as the output. + */ + private final String name; + + /** + * Use the factory method instead of this constructor. + * @param name any not null string. + */ + private TailNameFormatter(final String name) { + assert name != null; + this.name = name; + } + + @Override + public final String format(LogRecord record) { + return ""; + } + + @Override + public final String getTail(Handler h) { + return name; + } + + /** + * Equals method. + * @param o the other object. + * @return true if equal + * @since JavaMail 1.4.4 + */ + @Override + public final boolean equals(Object o) { + if (o instanceof TailNameFormatter) { + return name.equals(((TailNameFormatter) o).name); + } + return false; + } + + /** + * Hash code method. + * @return the hash code. + * @since JavaMail 1.4.4 + */ + @Override + public final int hashCode() { + return getClass().hashCode() + name.hashCode(); + } + + @Override + public final String toString() { + return name; + } + } +} diff --git a/app/src/main/java/com/sun/mail/util/logging/SeverityComparator.java b/app/src/main/java/com/sun/mail/util/logging/SeverityComparator.java new file mode 100644 index 0000000000..338a780bff --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/logging/SeverityComparator.java @@ -0,0 +1,332 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2018 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import java.io.Serializable; +import java.util.Comparator; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +/** + * Orders log records by level, thrown, sequence, and time. + * + * This comparator orders LogRecords by how severely each is attributed to + * failures in a program. The primary ordering is determined by the use of the + * logging API throughout a program by specifying a level to each log message. + * The secondary ordering is determined at runtime by the type of errors and + * exceptions generated by the program. The remaining ordering assumes that + * older log records are less severe than newer log records. + * + *

+ * The following LogRecord properties determine severity ordering: + *

    + *
  1. The natural comparison of the LogRecord + * {@linkplain Level#intValue level}. + *
  2. The expected recovery order of {@linkplain LogRecord#getThrown() thrown} + * property of a LogRecord and its cause chain. This ordering is derived from + * the JLS 11.1.1. The Kinds of Exceptions and JLS 11.5 The Exception Hierarchy. + * This is performed by {@linkplain #apply(java.lang.Throwable) finding} the + * throwable that best describes the entire cause chain. Once a specific + * throwable of each chain is identified it is then ranked lowest to highest by + * the following rules: + * + *
      + *
    • All LogRecords with a {@code Throwable} defined as + * "{@link #isNormal(java.lang.Throwable) normal occurrence}". + *
    • All LogRecords that do not have a thrown object. + *
    • All checked exceptions. This is any class that is assignable to the + * {@code java.lang.Throwable} class and is not a + * {@code java.lang.RuntimeException} or a {@code java.lang.Error}. + *
    • All unchecked exceptions. This is all {@code java.lang.RuntimeException} + * objects. + *
    • All errors that indicate a serious problem. This is all + * {@code java.lang.Error} objects. + *
    + *
  3. The natural comparison of the LogRecord + * {@linkplain LogRecord#getSequenceNumber() sequence}. + *
  4. The natural comparison of the LogRecord + * {@linkplain LogRecord#getMillis() millis}. + *
+ * + * @author Jason Mehrens + * @since JavaMail 1.5.2 + */ +public class SeverityComparator implements Comparator, Serializable { + + /** + * The generated serial version UID. + */ + private static final long serialVersionUID = -2620442245251791965L; + + /** + * A single instance that is shared among the logging package. + * The field is declared as java.util.Comparator so + * WebappClassLoader.clearReferencesStaticFinal() method will ignore this + * field. + */ + private static final Comparator INSTANCE + = new SeverityComparator(); + + /** + * A shared instance of a SeverityComparator. This is package private so the + * public API is not polluted with more methods. + * + * @return a shared instance of a SeverityComparator. + */ + static SeverityComparator getInstance() { + return (SeverityComparator) INSTANCE; + } + + /** + * Identifies a single throwable that best describes the given throwable and + * the entire {@linkplain Throwable#getCause() cause} chain. This method can + * be overridden to change the behavior of + * {@link #compare(java.util.logging.LogRecord, java.util.logging.LogRecord)}. + * + * @param chain the throwable or null. + * @return null if null was given, otherwise the throwable that best + * describes the entire chain. + * @see #isNormal(java.lang.Throwable) + */ + public Throwable apply(final Throwable chain) { + //Matches the j.u.f.UnaryOperator interface. + int limit = 0; + Throwable root = chain; + Throwable high = null; + Throwable normal = null; + for (Throwable cause = chain; cause != null; cause = cause.getCause()) { + root = cause; //Find the deepest cause. + + //Find the deepest nomral occurrance. + if (isNormal(cause)) { + normal = cause; + } + + //Find the deepest error that happened before a normal occurance. + if (normal == null && cause instanceof Error) { + high = cause; + } + + //Deal with excessive cause chains and cyclic throwables. + if (++limit == (1 << 16)) { + break; //Give up. + } + } + return high != null ? high : normal != null ? normal : root; + } + + /** + * {@link #apply(java.lang.Throwable) Reduces} each throwable chain argument + * then compare each throwable result. + * + * @param tc1 the first throwable chain or null. + * @param tc2 the second throwable chain or null. + * @return a negative integer, zero, or a positive integer as the first + * argument is less than, equal to, or greater than the second. + * @see #apply(java.lang.Throwable) + * @see #compareThrowable(java.lang.Throwable, java.lang.Throwable) + */ + public final int applyThenCompare(Throwable tc1, Throwable tc2) { + return tc1 == tc2 ? 0 : compareThrowable(apply(tc1), apply(tc2)); + } + + /** + * Compares two throwable objects or null. This method does not + * {@link #apply(java.lang.Throwable) reduce} each argument before + * comparing. This is method can be overridden to change the behavior of + * {@linkplain #compare(LogRecord, LogRecord)}. + * + * @param t1 the first throwable or null. + * @param t2 the second throwable or null. + * @return a negative integer, zero, or a positive integer as the first + * argument is less than, equal to, or greater than the second. + * @see #isNormal(java.lang.Throwable) + */ + public int compareThrowable(final Throwable t1, final Throwable t2) { + if (t1 == t2) { //Reflexive test including null. + return 0; + } else { + //Only one or the other is null at this point. + //Force normal occurrence to be lower than null. + if (t1 == null) { + return isNormal(t2) ? 1 : -1; + } else { + if (t2 == null) { + return isNormal(t1) ? -1 : 1; + } + } + + //From this point on neither are null. + //Follow the shortcut if we can. + if (t1.getClass() == t2.getClass()) { + return 0; + } + + //Ensure normal occurrence flow control is ordered low. + if (isNormal(t1)) { + return isNormal(t2) ? 0 : -1; + } else { + if (isNormal(t2)) { + return 1; + } + } + + //Rank the two unidenticial throwables using the rules from + //JLS 11.1.1. The Kinds of Exceptions and + //JLS 11.5 The Exception Hierarchy. + if (t1 instanceof Error) { + return t2 instanceof Error ? 0 : 1; + } else if (t1 instanceof RuntimeException) { + return t2 instanceof Error ? -1 + : t2 instanceof RuntimeException ? 0 : 1; + } else { + return t2 instanceof Error + || t2 instanceof RuntimeException ? -1 : 0; + } + } + } + + /** + * Compares two log records based on severity. + * + * @param o1 the first log record. + * @param o2 the second log record. + * @return a negative integer, zero, or a positive integer as the first + * argument is less than, equal to, or greater than the second. + * @throws NullPointerException if either argument is null. + */ + @SuppressWarnings("override") //JDK-6954234 + public int compare(final LogRecord o1, final LogRecord o2) { + if (o1 == null || o2 == null) { //Don't allow null. + throw new NullPointerException(toString(o1, o2)); + } + + /** + * LogRecords are mutable so a reflexive relationship test is a safety + * requirement. + */ + if (o1 == o2) { + return 0; + } + + int cmp = compare(o1.getLevel(), o2.getLevel()); + if (cmp == 0) { + cmp = applyThenCompare(o1.getThrown(), o2.getThrown()); + if (cmp == 0) { + cmp = compare(o1.getSequenceNumber(), o2.getSequenceNumber()); + if (cmp == 0) { + cmp = compare(o1.getMillis(), o2.getMillis()); + } + } + } + return cmp; + } + + /** + * Determines if the given object is also a comparator and it imposes the + * same ordering as this comparator. + * + * @param o the reference object with which to compare. + * @return true if this object equal to the argument; false otherwise. + */ + @Override + public boolean equals(final Object o) { + return o == null ? false : o.getClass() == getClass(); + } + + /** + * Returns a hash code value for the object. + * + * @return Returns a hash code value for the object. + */ + @Override + public int hashCode() { + return 31 * getClass().hashCode(); + } + + /** + * Determines if the given throwable instance is "normal occurrence". This + * is any checked or unchecked exception with 'Interrupt' in the class name + * or ancestral class name. Any {@code java.lang.ThreadDeath} object or + * subclasses. + * + * This method can be overridden to change the behavior of the + * {@linkplain #apply(java.lang.Throwable)} method. + * + * @param t a throwable or null. + * @return true the given throwable is a "normal occurrence". + */ + public boolean isNormal(final Throwable t) { + if (t == null) { //This is only needed when called directly. + return false; + } + + /** + * Use the class names to avoid loading more classes. + */ + final Class root = Throwable.class; + final Class error = Error.class; + for (Class c = t.getClass(); c != root; c = c.getSuperclass()) { + if (error.isAssignableFrom(c)) { + if (c.getName().equals("java.lang.ThreadDeath")) { + return true; + } + } else { + //Interrupt, Interrupted or Interruption. + if (c.getName().contains("Interrupt")) { + return true; + } + } + } + return false; + } + + /** + * Compare two level objects. + * + * @param a the first level. + * @param b the second level. + * @return a negative integer, zero, or a positive integer as the first + * argument is less than, equal to, or greater than the second. + */ + private int compare(final Level a, final Level b) { + return a == b ? 0 : compare(a.intValue(), b.intValue()); + } + + /** + * Outline the message create string. + * + * @param o1 argument one. + * @param o2 argument two. + * @return the message string. + */ + private static String toString(final Object o1, final Object o2) { + return o1 + ", " + o2; + } + + /** + * Compare two longs. Can be removed when JDK 1.7 is required. + * + * @param x the first long. + * @param y the second long. + * @return a negative integer, zero, or a positive integer as the first + * argument is less than, equal to, or greater than the second. + */ + private int compare(final long x, final long y) { + return x < y ? -1 : x > y ? 1 : 0; + } +} diff --git a/app/src/main/java/com/sun/mail/util/logging/package.html b/app/src/main/java/com/sun/mail/util/logging/package.html new file mode 100644 index 0000000000..fcde4a4ac3 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/logging/package.html @@ -0,0 +1,34 @@ + + + + + + + + + + Contains Jakarta Mail extensions for + the Java™ platform's core logging + facilities. This package contains classes used to export log messages + as a formatted email message. Classes in this package typically use + LogManager properties to set default values; see the specific + documentation for each concrete class. + + diff --git a/app/src/main/java/com/sun/mail/util/package.html b/app/src/main/java/com/sun/mail/util/package.html new file mode 100644 index 0000000000..6f809d1f5c --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/package.html @@ -0,0 +1,61 @@ + + + + + + +com.sun.mail.util package + + + +

+Utility classes for use with the Jakarta Mail API. +These utility classes are not part of the Jakarta Mail specification. +While this package contains many classes used by the Jakarta Mail implementation +and not intended for direct use by applications, the classes documented +here may be of use to applications. +

+

+Classes in this package log debugging information using +{@link java.util.logging} as described in the following table: +

+ + + + + + + + + + + + + +
com.sun.mail.util Loggers
Logger NameLogging LevelPurpose
com.sun.mail.util.socketFINERDebugging output related to creating sockets
+ +

+WARNING: The APIs in this package should be +considered EXPERIMENTAL. They may be changed in the +future in ways that are incompatible with applications using the +current APIs. +

+ + + diff --git a/app/src/main/java/javax/mail/Address.java b/app/src/main/java/javax/mail/Address.java new file mode 100644 index 0000000000..2b87cf4e30 --- /dev/null +++ b/app/src/main/java/javax/mail/Address.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.io.Serializable; + +/** + * This abstract class models the addresses in a message. + * Subclasses provide specific implementations. Subclasses + * will typically be serializable so that (for example) the + * use of Address objects in search terms can be serialized + * along with the search terms. + * + * @author John Mani + * @author Bill Shannon + */ + +public abstract class Address implements Serializable { + + private static final long serialVersionUID = -5822459626751992278L; + + /** + * Return a type string that identifies this address type. + * + * @return address type + * @see javax.mail.internet.InternetAddress + */ + public abstract String getType(); + + /** + * Return a String representation of this address object. + * + * @return string representation of this address + */ + @Override + public abstract String toString(); + + /** + * The equality operator. Subclasses should provide an + * implementation of this method that supports value equality + * (do the two Address objects represent the same destination?), + * not object reference equality. A subclass must also provide + * a corresponding implementation of the hashCode + * method that preserves the general contract of + * equals and hashCode - objects that + * compare as equal must have the same hashCode. + * + * @param address Address object + */ + @Override + public abstract boolean equals(Object address); +} diff --git a/app/src/main/java/javax/mail/AuthenticationFailedException.java b/app/src/main/java/javax/mail/AuthenticationFailedException.java new file mode 100644 index 0000000000..e0de809e6f --- /dev/null +++ b/app/src/main/java/javax/mail/AuthenticationFailedException.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * This exception is thrown when the connect method on a Store or + * Transport object fails due to an authentication failure (e.g., + * bad user name or password). + * + * @author Bill Shannon + */ + +public class AuthenticationFailedException extends MessagingException { + + private static final long serialVersionUID = 492080754054436511L; + + /** + * Constructs an AuthenticationFailedException. + */ + public AuthenticationFailedException() { + super(); + } + + /** + * Constructs an AuthenticationFailedException with the specified + * detail message. + * + * @param message The detailed error message + */ + public AuthenticationFailedException(String message) { + super(message); + } + + /** + * Constructs an AuthenticationFailedException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param message The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public AuthenticationFailedException(String message, Exception e) { + super(message, e); + } +} diff --git a/app/src/main/java/javax/mail/Authenticator.java b/app/src/main/java/javax/mail/Authenticator.java new file mode 100644 index 0000000000..5fa783801c --- /dev/null +++ b/app/src/main/java/javax/mail/Authenticator.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.net.InetAddress; + +/** + * The class Authenticator represents an object that knows how to obtain + * authentication for a network connection. Usually, it will do this + * by prompting the user for information. + *

+ * Applications use this class by creating a subclass, and registering + * an instance of that subclass with the session when it is created. + * When authentication is required, the system will invoke a method + * on the subclass (like getPasswordAuthentication). The subclass's + * method can query about the authentication being requested with a + * number of inherited methods (getRequestingXXX()), and form an + * appropriate message for the user. + *

+ * All methods that request authentication have a default implementation + * that fails. + * + * @see java.net.Authenticator + * @see javax.mail.Session#getInstance(java.util.Properties, + * javax.mail.Authenticator) + * @see javax.mail.Session#getDefaultInstance(java.util.Properties, + * javax.mail.Authenticator) + * @see javax.mail.Session#requestPasswordAuthentication + * @see javax.mail.PasswordAuthentication + * + * @author Bill Foote + * @author Bill Shannon + */ + +// There are no abstract methods, but to be useful the user must +// subclass. +public abstract class Authenticator { + + private InetAddress requestingSite; + private int requestingPort; + private String requestingProtocol; + private String requestingPrompt; + private String requestingUserName; + + /** + * Ask the authenticator for a password. + *

+ * + * @param addr The InetAddress of the site requesting authorization, + * or null if not known. + * @param port the port for the requested connection + * @param protocol The protocol that's requesting the connection + * (@see java.net.Authenticator.getProtocol()) + * @param prompt A prompt string for the user + * + * @return The username/password, or null if one can't be gotten. + */ + final synchronized PasswordAuthentication requestPasswordAuthentication( + InetAddress addr, int port, String protocol, + String prompt, String defaultUserName) { + requestingSite = addr; + requestingPort = port; + requestingProtocol = protocol; + requestingPrompt = prompt; + requestingUserName = defaultUserName; + return getPasswordAuthentication(); + } + + /** + * @return the InetAddress of the site requesting authorization, or null + * if it's not available. + */ + protected final InetAddress getRequestingSite() { + return requestingSite; + } + + /** + * @return the port for the requested connection + */ + protected final int getRequestingPort() { + return requestingPort; + } + + /** + * Give the protocol that's requesting the connection. Often this + * will be based on a URLName. + * + * @return the protcol + * + * @see javax.mail.URLName#getProtocol + */ + protected final String getRequestingProtocol() { + return requestingProtocol; + } + + /** + * @return the prompt string given by the requestor + */ + protected final String getRequestingPrompt() { + return requestingPrompt; + } + + /** + * @return the default user name given by the requestor + */ + protected final String getDefaultUserName() { + return requestingUserName; + } + + /** + * Called when password authentication is needed. Subclasses should + * override the default implementation, which returns null.

+ * + * Note that if this method uses a dialog to prompt the user for this + * information, the dialog needs to block until the user supplies the + * information. This method can not simply return after showing the + * dialog. + * @return The PasswordAuthentication collected from the + * user, or null if none is provided. + */ + protected PasswordAuthentication getPasswordAuthentication() { + return null; + } +} diff --git a/app/src/main/java/javax/mail/BodyPart.java b/app/src/main/java/javax/mail/BodyPart.java new file mode 100644 index 0000000000..bb94f75e71 --- /dev/null +++ b/app/src/main/java/javax/mail/BodyPart.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * This class models a Part that is contained within a Multipart. + * This is an abstract class. Subclasses provide actual implementations.

+ * + * BodyPart implements the Part interface. Thus, it contains a set of + * attributes and a "content". + * + * @author John Mani + * @author Bill Shannon + */ + +public abstract class BodyPart implements Part { + + /** + * The Multipart object containing this BodyPart, + * if known. + * @since JavaMail 1.1 + */ + protected Multipart parent; + + /** + * Return the containing Multipart object, + * or null if not known. + * + * @return the parent Multipart + */ + public Multipart getParent() { + return parent; + } + + /** + * Set the parent of this BodyPart to be the specified + * Multipart. Normally called by Multipart's + * addBodyPart method. parent may be + * null if the BodyPart is being removed + * from its containing Multipart. + * @since JavaMail 1.1 + */ + void setParent(Multipart parent) { + this.parent = parent; + } +} diff --git a/app/src/main/java/javax/mail/EncodingAware.java b/app/src/main/java/javax/mail/EncodingAware.java new file mode 100644 index 0000000000..27efc70051 --- /dev/null +++ b/app/src/main/java/javax/mail/EncodingAware.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * A {@link javax.activation.DataSource DataSource} that also implements + * EncodingAware may specify the Content-Transfer-Encoding + * to use for its data. Valid Content-Transfer-Encoding values specified + * by RFC 2045 are "7bit", "8bit", "quoted-printable", "base64", and "binary". + *

+ * For example, a {@link javax.activation.FileDataSource FileDataSource} + * could be created that forces all files to be base64 encoded: + *

+ *  public class Base64FileDataSource extends FileDataSource
+ *					implements EncodingAware {
+ *	public Base64FileDataSource(File file) {
+ *	    super(file);
+ *	}
+ *
+ *	// implements EncodingAware.getEncoding()
+ *	public String getEncoding() {
+ *	    return "base64";
+ *	}
+ *  }
+ * 

+ * + * @since JavaMail 1.5 + * @author Bill Shannon + */ + +public interface EncodingAware { + + /** + * Return the MIME Content-Transfer-Encoding to use for this data, + * or null to indicate that an appropriate value should be chosen + * by the caller. + * + * @return the Content-Transfer-Encoding value, or null + */ + public String getEncoding(); +} diff --git a/app/src/main/java/javax/mail/EventQueue.java b/app/src/main/java/javax/mail/EventQueue.java new file mode 100644 index 0000000000..618f872c8e --- /dev/null +++ b/app/src/main/java/javax/mail/EventQueue.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.util.EventListener; +import java.util.Vector; +import java.util.Queue; +import java.util.WeakHashMap; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.Executor; +import javax.mail.event.MailEvent; + +/** + * Package private class used by Store & Folder to dispatch events. + * This class implements an event queue, and a dispatcher thread that + * dequeues and dispatches events from the queue. + * + * @author Bill Shannon + */ +class EventQueue implements Runnable { + + private volatile BlockingQueue q; + private Executor executor; + + private static WeakHashMap appq; + + /** + * A special event that causes the queue processing task to terminate. + */ + static class TerminatorEvent extends MailEvent { + private static final long serialVersionUID = -2481895000841664111L; + + TerminatorEvent() { + super(new Object()); + } + + @Override + public void dispatch(Object listener) { + // Kill the event dispatching thread. + Thread.currentThread().interrupt(); + } + } + + /** + * A "struct" to put on the queue. + */ + static class QueueElement { + MailEvent event = null; + Vector vector = null; + + QueueElement(MailEvent event, Vector vector) { + this.event = event; + this.vector = vector; + } + } + + /** + * Construct an EventQueue using the specified Executor. + * If the Executor is null, threads will be created as needed. + */ + EventQueue(Executor ex) { + this.executor = ex; + } + + /** + * Enqueue an event. + */ + synchronized void enqueue(MailEvent event, + Vector vector) { + // if this is the first event, create the queue and start the event task + if (q == null) { + q = new LinkedBlockingQueue<>(); + if (executor != null) { + executor.execute(this); + } else { + Thread qThread = new Thread(this, "Jakarta-Mail-EventQueue"); + qThread.setDaemon(true); // not a user thread + qThread.start(); + } + } + q.add(new QueueElement(event, vector)); + } + + /** + * Terminate the task running the queue, but only if there is a queue. + */ + synchronized void terminateQueue() { + if (q != null) { + Vector dummyListeners = new Vector<>(); + dummyListeners.setSize(1); // need atleast one listener + q.add(new QueueElement(new TerminatorEvent(), dummyListeners)); + q = null; + } + } + + /** + * Create (if necessary) an application-scoped event queue. + * Application scoping is based on the thread's context class loader. + */ + static synchronized EventQueue getApplicationEventQueue(Executor ex) { + ClassLoader cl = Session.getContextClassLoader(); + if (appq == null) + appq = new WeakHashMap<>(); + EventQueue q = appq.get(cl); + if (q == null) { + q = new EventQueue(ex); + appq.put(cl, q); + } + return q; + } + + /** + * Pull events off the queue and dispatch them. + */ + @Override + public void run() { + + BlockingQueue bq = q; + if (bq == null) + return; + try { + loop: + for (;;) { + // block until an item is available + QueueElement qe = bq.take(); + MailEvent e = qe.event; + Vector v = qe.vector; + + for (int i = 0; i < v.size(); i++) + try { + e.dispatch(v.elementAt(i)); + } catch (Throwable t) { + if (t instanceof InterruptedException) + break loop; + // ignore anything else thrown by the listener + } + + qe = null; e = null; v = null; + } + } catch (InterruptedException e) { + // just die + } + } +} diff --git a/app/src/main/java/javax/mail/FetchProfile.java b/app/src/main/java/javax/mail/FetchProfile.java new file mode 100644 index 0000000000..1b494545fe --- /dev/null +++ b/app/src/main/java/javax/mail/FetchProfile.java @@ -0,0 +1,223 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.util.Vector; + +/** + * Clients use a FetchProfile to list the Message attributes that + * it wishes to prefetch from the server for a range of messages.

+ * + * Messages obtained from a Folder are light-weight objects that + * typically start off as empty references to the actual messages. + * Such a Message object is filled in "on-demand" when the appropriate + * get*() methods are invoked on that particular Message. Certain + * server-based message access protocols (Ex: IMAP) allow batch + * fetching of message attributes for a range of messages in a single + * request. Clients that want to use message attributes for a range of + * Messages (Example: to display the top-level headers in a headerlist) + * might want to use the optimization provided by such servers. The + * FetchProfile allows the client to indicate this desire + * to the server.

+ * + * Note that implementations are not obligated to support + * FetchProfiles, since there might be cases where the backend service + * does not allow easy, efficient fetching of such profiles.

+ * + * Sample code that illustrates the use of a FetchProfile is given + * below: + *

+ *
+ *
+ *  Message[] msgs = folder.getMessages();
+ *
+ *  FetchProfile fp = new FetchProfile();
+ *  fp.add(FetchProfile.Item.ENVELOPE);
+ *  fp.add("X-mailer");
+ *  folder.fetch(msgs, fp);
+ *
+ * 

+ * + * @see javax.mail.Folder#fetch + * @author John Mani + * @author Bill Shannon + */ + +public class FetchProfile { + + private Vector specials; // specials + private Vector headers; // vector of header names + + /** + * This inner class is the base class of all items that + * can be requested in a FetchProfile. The items currently + * defined here are ENVELOPE, CONTENT_INFO + * and FLAGS. The UIDFolder interface + * defines the UID Item as well.

+ * + * Note that this class only has a protected constructor, therby + * restricting new Item types to either this class or subclasses. + * This effectively implements a enumeration of allowed Item types. + * + * @see UIDFolder + */ + + public static class Item { + /** + * This is the Envelope item.

+ * + * The Envelope is an aggregration of the common attributes + * of a Message. Implementations should include the following + * attributes: From, To, Cc, Bcc, ReplyTo, Subject and Date. + * More items may be included as well.

+ * + * For implementations of the IMAP4 protocol (RFC 2060), the + * Envelope should include the ENVELOPE data item. More items + * may be included too. + */ + public static final Item ENVELOPE = new Item("ENVELOPE"); + + /** + * This item is for fetching information about the + * content of the message.

+ * + * This includes all the attributes that describe the content + * of the message. Implementations should include the following + * attributes: ContentType, ContentDisposition, + * ContentDescription, Size and LineCount. Other items may be + * included as well. + */ + public static final Item CONTENT_INFO = new Item("CONTENT_INFO"); + + /** + * SIZE is a fetch profile item that can be included in a + * FetchProfile during a fetch request to a Folder. + * This item indicates that the sizes of the messages in the specified + * range should be prefetched.

+ * + * @since JavaMail 1.5 + */ + public static final Item SIZE = new Item("SIZE"); + + /** + * This is the Flags item. + */ + public static final Item FLAGS = new Item("FLAGS"); + + private String name; + + /** + * Constructor for an item. The name is used only for debugging. + * + * @param name the item name + */ + protected Item(String name) { + this.name = name; + } + + /** + * Include the name in the toString return value for debugging. + */ + @Override + public String toString() { + return getClass().getName() + "[" + name + "]"; + } + } + + /** + * Create an empty FetchProfile. + */ + public FetchProfile() { + specials = null; + headers = null; + } + + /** + * Add the given special item as one of the attributes to + * be prefetched. + * + * @param item the special item to be fetched + * @see FetchProfile.Item#ENVELOPE + * @see FetchProfile.Item#CONTENT_INFO + * @see FetchProfile.Item#FLAGS + */ + public void add(Item item) { + if (specials == null) + specials = new Vector<>(); + specials.addElement(item); + } + + /** + * Add the specified header-field to the list of attributes + * to be prefetched. + * + * @param headerName header to be prefetched + */ + public void add(String headerName) { + if (headers == null) + headers = new Vector<>(); + headers.addElement(headerName); + } + + /** + * Returns true if the fetch profile contains the given special item. + * + * @param item the Item to test + * @return true if the fetch profile contains the given special item + */ + public boolean contains(Item item) { + return specials != null && specials.contains(item); + } + + /** + * Returns true if the fetch profile contains the given header name. + * + * @param headerName the header to test + * @return true if the fetch profile contains the given header name + */ + public boolean contains(String headerName) { + return headers != null && headers.contains(headerName); + } + + /** + * Get the items set in this profile. + * + * @return items set in this profile + */ + public Item[] getItems() { + if (specials == null) + return new Item[0]; + + Item[] s = new Item[specials.size()]; + specials.copyInto(s); + return s; + } + + /** + * Get the names of the header-fields set in this profile. + * + * @return headers set in this profile + */ + public String[] getHeaderNames() { + if (headers == null) + return new String[0]; + + String[] s = new String[headers.size()]; + headers.copyInto(s); + return s; + } +} diff --git a/app/src/main/java/javax/mail/Flags.java b/app/src/main/java/javax/mail/Flags.java new file mode 100644 index 0000000000..68e88b76f8 --- /dev/null +++ b/app/src/main/java/javax/mail/Flags.java @@ -0,0 +1,667 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.io.Serializable; +import java.util.*; + +/** + * The Flags class represents the set of flags on a Message. Flags + * are composed of predefined system flags, and user defined flags.

+ * + * A System flag is represented by the Flags.Flag + * inner class. A User defined flag is represented as a String. + * User flags are case-independent.

+ * + * A set of standard system flags are predefined. Most folder + * implementations are expected to support these flags. Some + * implementations may also support arbitrary user-defined flags. The + * getPermanentFlags method on a Folder returns a Flags + * object that holds all the flags that are supported by that folder + * implementation.

+ * + * A Flags object is serializable so that (for example) the + * use of Flags objects in search terms can be serialized + * along with the search terms.

+ * + * Warning: + * Serialized objects of this class may not be compatible with future + * Jakarta Mail API releases. The current serialization support is + * appropriate for short term storage.

+ * + * The below code sample illustrates how to set, examine, and get the + * flags for a message. + *

+ *
+ * Message m = folder.getMessage(1);
+ * m.setFlag(Flags.Flag.DELETED, true); // set the DELETED flag
+ *
+ * // Check if DELETED flag is set on this message
+ * if (m.isSet(Flags.Flag.DELETED))
+ *	System.out.println("DELETED message");
+ *
+ * // Examine ALL system flags for this message
+ * Flags flags = m.getFlags();
+ * Flags.Flag[] sf = flags.getSystemFlags();
+ * for (int i = 0; i < sf.length; i++) {
+ *	if (sf[i] == Flags.Flag.DELETED)
+ *            System.out.println("DELETED message");
+ *	else if (sf[i] == Flags.Flag.SEEN)
+ *            System.out.println("SEEN message");
+ *      ......
+ *      ......
+ * }
+ * 
+ *

+ * + * @see Folder#getPermanentFlags + * @author John Mani + * @author Bill Shannon + */ + +public class Flags implements Cloneable, Serializable { + + private int system_flags = 0; + // used as a case-independent Set that preserves the original case, + // the key is the lowercase flag name and the value is the original + private Hashtable user_flags = null; + + private final static int ANSWERED_BIT = 0x01; + private final static int DELETED_BIT = 0x02; + private final static int DRAFT_BIT = 0x04; + private final static int FLAGGED_BIT = 0x08; + private final static int RECENT_BIT = 0x10; + private final static int SEEN_BIT = 0x20; + private final static int USER_BIT = 0x80000000; + + private static final long serialVersionUID = 6243590407214169028L; + + /** + * This inner class represents an individual system flag. A set + * of standard system flag objects are predefined here. + */ + public static final class Flag { + /** + * This message has been answered. This flag is set by clients + * to indicate that this message has been answered to. + */ + public static final Flag ANSWERED = new Flag(ANSWERED_BIT); + + /** + * This message is marked deleted. Clients set this flag to + * mark a message as deleted. The expunge operation on a folder + * removes all messages in that folder that are marked for deletion. + */ + public static final Flag DELETED = new Flag(DELETED_BIT); + + /** + * This message is a draft. This flag is set by clients + * to indicate that the message is a draft message. + */ + public static final Flag DRAFT = new Flag(DRAFT_BIT); + + /** + * This message is flagged. No semantic is defined for this flag. + * Clients alter this flag. + */ + public static final Flag FLAGGED = new Flag(FLAGGED_BIT); + + /** + * This message is recent. Folder implementations set this flag + * to indicate that this message is new to this folder, that is, + * it has arrived since the last time this folder was opened.

+ * + * Clients cannot alter this flag. + */ + public static final Flag RECENT = new Flag(RECENT_BIT); + + /** + * This message is seen. This flag is implicitly set by the + * implementation when this Message's content is returned + * to the client in some form. The getInputStream + * and getContent methods on Message cause this + * flag to be set.

+ * + * Clients can alter this flag. + */ + public static final Flag SEEN = new Flag(SEEN_BIT); + + /** + * A special flag that indicates that this folder supports + * user defined flags.

+ * + * The implementation sets this flag. Clients cannot alter + * this flag but can use it to determine if a folder supports + * user defined flags by using + * folder.getPermanentFlags().contains(Flags.Flag.USER). + */ + public static final Flag USER = new Flag(USER_BIT); + + // flags are stored as bits for efficiency + private int bit; + private Flag(int bit) { + this.bit = bit; + } + } + + + /** + * Construct an empty Flags object. + */ + public Flags() { } + + /** + * Construct a Flags object initialized with the given flags. + * + * @param flags the flags for initialization + */ + @SuppressWarnings("unchecked") + public Flags(Flags flags) { + this.system_flags = flags.system_flags; + if (flags.user_flags != null) + this.user_flags = (Hashtable)flags.user_flags.clone(); + } + + /** + * Construct a Flags object initialized with the given system flag. + * + * @param flag the flag for initialization + */ + public Flags(Flag flag) { + this.system_flags |= flag.bit; + } + + /** + * Construct a Flags object initialized with the given user flag. + * + * @param flag the flag for initialization + */ + public Flags(String flag) { + user_flags = new Hashtable<>(1); + user_flags.put(flag.toLowerCase(Locale.ENGLISH), flag); + } + + /** + * Add the specified system flag to this Flags object. + * + * @param flag the flag to add + */ + public void add(Flag flag) { + system_flags |= flag.bit; + } + + /** + * Add the specified user flag to this Flags object. + * + * @param flag the flag to add + */ + public void add(String flag) { + if (user_flags == null) + user_flags = new Hashtable<>(1); + user_flags.put(flag.toLowerCase(Locale.ENGLISH), flag); + } + + /** + * Add all the flags in the given Flags object to this + * Flags object. + * + * @param f Flags object + */ + public void add(Flags f) { + system_flags |= f.system_flags; // add system flags + + if (f.user_flags != null) { // add user-defined flags + if (user_flags == null) + user_flags = new Hashtable<>(1); + + Enumeration e = f.user_flags.keys(); + + while (e.hasMoreElements()) { + String s = e.nextElement(); + user_flags.put(s, f.user_flags.get(s)); + } + } + } + + /** + * Remove the specified system flag from this Flags object. + * + * @param flag the flag to be removed + */ + public void remove(Flag flag) { + system_flags &= ~flag.bit; + } + + /** + * Remove the specified user flag from this Flags object. + * + * @param flag the flag to be removed + */ + public void remove(String flag) { + if (user_flags != null) + user_flags.remove(flag.toLowerCase(Locale.ENGLISH)); + } + + /** + * Remove all flags in the given Flags object from this + * Flags object. + * + * @param f the flag to be removed + */ + public void remove(Flags f) { + system_flags &= ~f.system_flags; // remove system flags + + if (f.user_flags != null) { + if (user_flags == null) + return; + + Enumeration e = f.user_flags.keys(); + while (e.hasMoreElements()) + user_flags.remove(e.nextElement()); + } + } + + /** + * Remove any flags not in the given Flags object. + * Useful for clearing flags not supported by a server. If the + * given Flags object includes the Flags.Flag.USER flag, all user + * flags in this Flags object are retained. + * + * @param f the flags to keep + * @return true if this Flags object changed + * @since JavaMail 1.6 + */ + public boolean retainAll(Flags f) { + boolean changed = false; + int sf = system_flags & f.system_flags; + if (system_flags != sf) { + system_flags = sf; + changed = true; + } + + // if we have user flags, and the USER flag is not set in "f", + // determine which user flags to clear + if (user_flags != null && (f.system_flags & USER_BIT) == 0) { + if (f.user_flags != null) { + Enumeration e = user_flags.keys(); + while (e.hasMoreElements()) { + String key = e.nextElement(); + if (!f.user_flags.containsKey(key)) { + user_flags.remove(key); + changed = true; + } + } + } else { + // if anything in user_flags, throw them away + changed = user_flags.size() > 0; + user_flags = null; + } + } + return changed; + } + + /** + * Check whether the specified system flag is present in this Flags object. + * + * @param flag the flag to test + * @return true of the given flag is present, otherwise false. + */ + public boolean contains(Flag flag) { + return (system_flags & flag.bit) != 0; + } + + /** + * Check whether the specified user flag is present in this Flags object. + * + * @param flag the flag to test + * @return true of the given flag is present, otherwise false. + */ + public boolean contains(String flag) { + if (user_flags == null) + return false; + else + return user_flags.containsKey(flag.toLowerCase(Locale.ENGLISH)); + } + + /** + * Check whether all the flags in the specified Flags object are + * present in this Flags object. + * + * @param f the flags to test + * @return true if all flags in the given Flags object are present, + * otherwise false. + */ + public boolean contains(Flags f) { + // Check system flags + if ((f.system_flags & system_flags) != f.system_flags) + return false; + + // Check user flags + if (f.user_flags != null) { + if (user_flags == null) + return false; + Enumeration e = f.user_flags.keys(); + + while (e.hasMoreElements()) { + if (!user_flags.containsKey(e.nextElement())) + return false; + } + } + + // If we've made it till here, return true + return true; + } + + /** + * Check whether the two Flags objects are equal. + * + * @return true if they're equal + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Flags)) + return false; + + Flags f = (Flags)obj; + + // Check system flags + if (f.system_flags != this.system_flags) + return false; + + // Check user flags + int size = this.user_flags == null ? 0 : this.user_flags.size(); + int fsize = f.user_flags == null ? 0 : f.user_flags.size(); + if (size == 0 && fsize == 0) + return true; + if (f.user_flags != null && this.user_flags != null && fsize == size) + return user_flags.keySet().equals(f.user_flags.keySet()); + + return false; + } + + /** + * Compute a hash code for this Flags object. + * + * @return the hash code + */ + @Override + public int hashCode() { + int hash = system_flags; + if (user_flags != null) { + Enumeration e = user_flags.keys(); + while (e.hasMoreElements()) + hash += e.nextElement().hashCode(); + } + return hash; + } + + /** + * Return all the system flags in this Flags object. Returns + * an array of size zero if no flags are set. + * + * @return array of Flags.Flag objects representing system flags + */ + public Flag[] getSystemFlags() { + Vector v = new Vector<>(); + if ((system_flags & ANSWERED_BIT) != 0) + v.addElement(Flag.ANSWERED); + if ((system_flags & DELETED_BIT) != 0) + v.addElement(Flag.DELETED); + if ((system_flags & DRAFT_BIT) != 0) + v.addElement(Flag.DRAFT); + if ((system_flags & FLAGGED_BIT) != 0) + v.addElement(Flag.FLAGGED); + if ((system_flags & RECENT_BIT) != 0) + v.addElement(Flag.RECENT); + if ((system_flags & SEEN_BIT) != 0) + v.addElement(Flag.SEEN); + if ((system_flags & USER_BIT) != 0) + v.addElement(Flag.USER); + + Flag[] f = new Flag[v.size()]; + v.copyInto(f); + return f; + } + + /** + * Return all the user flags in this Flags object. Returns + * an array of size zero if no flags are set. + * + * @return array of Strings, each String represents a flag. + */ + public String[] getUserFlags() { + Vector v = new Vector<>(); + if (user_flags != null) { + Enumeration e = user_flags.elements(); + + while (e.hasMoreElements()) + v.addElement(e.nextElement()); + } + + String[] f = new String[v.size()]; + v.copyInto(f); + return f; + } + + /** + * Clear all of the system flags. + * + * @since JavaMail 1.6 + */ + public void clearSystemFlags() { + system_flags = 0; + } + + /** + * Clear all of the user flags. + * + * @since JavaMail 1.6 + */ + public void clearUserFlags() { + user_flags = null; + } + + /** + * Returns a clone of this Flags object. + */ + @SuppressWarnings("unchecked") + @Override + public Object clone() { + Flags f = null; + try { + f = (Flags)super.clone(); + } catch (CloneNotSupportedException cex) { + // ignore, can't happen + } + if (this.user_flags != null) + f.user_flags = (Hashtable)this.user_flags.clone(); + return f; + } + + /** + * Return a string representation of this Flags object. + * Note that the exact format of the string is subject to change. + */ + public String toString() { + StringBuilder sb = new StringBuilder(); + + if ((system_flags & ANSWERED_BIT) != 0) + sb.append("\\Answered "); + if ((system_flags & DELETED_BIT) != 0) + sb.append("\\Deleted "); + if ((system_flags & DRAFT_BIT) != 0) + sb.append("\\Draft "); + if ((system_flags & FLAGGED_BIT) != 0) + sb.append("\\Flagged "); + if ((system_flags & RECENT_BIT) != 0) + sb.append("\\Recent "); + if ((system_flags & SEEN_BIT) != 0) + sb.append("\\Seen "); + if ((system_flags & USER_BIT) != 0) + sb.append("\\* "); + + boolean first = true; + if (user_flags != null) { + Enumeration e = user_flags.elements(); + + while (e.hasMoreElements()) { + if (first) + first = false; + else + sb.append(' '); + sb.append(e.nextElement()); + } + } + + if (first && sb.length() > 0) + sb.setLength(sb.length() - 1); // smash trailing space + + return sb.toString(); + } + + /***** + public static void main(String argv[]) throws Exception { + // a new flags object + Flags f1 = new Flags(); + f1.add(Flags.Flag.DELETED); + f1.add(Flags.Flag.SEEN); + f1.add(Flags.Flag.RECENT); + f1.add(Flags.Flag.ANSWERED); + + // check copy constructor with only system flags + Flags fc = new Flags(f1); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // check clone with only system flags + fc = (Flags)f1.clone(); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // add a user flag and make sure it still works right + f1.add("MyFlag"); + + // shouldn't be equal here + if (!f1.equals(fc) && !fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // check clone + fc = (Flags)f1.clone(); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // make sure user flag hash tables are separate + fc.add("AnotherFlag"); + if (!f1.equals(fc) && !fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // check copy constructor + fc = new Flags(f1); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // another new flags object + Flags f2 = new Flags(Flags.Flag.ANSWERED); + f2.add("MyFlag"); + + if (f1.contains(Flags.Flag.DELETED)) + System.out.println("success"); + else + System.out.println("fail"); + + if (f1.contains(Flags.Flag.SEEN)) + System.out.println("success"); + else + System.out.println("fail"); + + if (f1.contains(Flags.Flag.RECENT)) + System.out.println("success"); + else + System.out.println("fail"); + + if (f1.contains("MyFlag")) + System.out.println("success"); + else + System.out.println("fail"); + + if (f2.contains(Flags.Flag.ANSWERED)) + System.out.println("success"); + else + System.out.println("fail"); + + + System.out.println("----------------"); + + String[] s = f1.getUserFlags(); + for (int i = 0; i < s.length; i++) + System.out.println(s[i]); + System.out.println("----------------"); + s = f2.getUserFlags(); + for (int i = 0; i < s.length; i++) + System.out.println(s[i]); + + System.out.println("----------------"); + + if (f1.contains(f2)) // this should be true + System.out.println("success"); + else + System.out.println("fail"); + + if (!f2.contains(f1)) // this should be false + System.out.println("success"); + else + System.out.println("fail"); + + Flags f3 = new Flags(); + f3.add(Flags.Flag.DELETED); + f3.add(Flags.Flag.SEEN); + f3.add(Flags.Flag.RECENT); + f3.add(Flags.Flag.ANSWERED); + f3.add("ANOTHERFLAG"); + f3.add("MYFLAG"); + + f1.add("AnotherFlag"); + + if (f1.equals(f3)) + System.out.println("equals success"); + else + System.out.println("fail"); + if (f3.equals(f1)) + System.out.println("equals success"); + else + System.out.println("fail"); + System.out.println("f1 hash code " + f1.hashCode()); + System.out.println("f3 hash code " + f3.hashCode()); + if (f1.hashCode() == f3.hashCode()) + System.out.println("success"); + else + System.out.println("fail"); + } + ****/ +} diff --git a/app/src/main/java/javax/mail/Folder.java b/app/src/main/java/javax/mail/Folder.java new file mode 100644 index 0000000000..0ddce216fc --- /dev/null +++ b/app/src/main/java/javax/mail/Folder.java @@ -0,0 +1,1657 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.lang.*; +import java.util.ArrayList; +import java.util.EventListener; +import java.util.List; +import java.util.Vector; +import java.util.concurrent.Executor; +import javax.mail.search.SearchTerm; +import javax.mail.event.*; + +/** + * Folder is an abstract class that represents a folder for mail + * messages. Subclasses implement protocol specific Folders.

+ * + * Folders can contain Messages, other Folders or both, thus providing + * a tree-like hierarchy rooted at the Store's default folder. (Note + * that some Folder implementations may not allow both Messages and + * other Folders in the same Folder).

+ * + * The interpretation of folder names is implementation dependent. + * The different levels of hierarchy in a folder's full name + * are separated from each other by the hierarchy delimiter + * character.

+ * + * The case-insensitive full folder name (that is, the full name + * relative to the default folder for a Store) INBOX + * is reserved to mean the "primary folder for this user on this + * server". Not all Stores will provide an INBOX folder, and not + * all users will have an INBOX folder at all times. The name + * INBOX is reserved to refer to this folder, + * when it exists, in Stores that provide it.

+ * + * A Folder object obtained from a Store need not actually exist + * in the backend store. The exists method tests whether + * the folder exists or not. The create method + * creates a Folder.

+ * + * A Folder is initially in the closed state. Certain methods are valid + * in this state; the documentation for those methods note this. A + * Folder is opened by calling its 'open' method. All Folder methods, + * except open, delete and + * renameTo, are valid in this state.

+ * + * The only way to get a Folder is by invoking the + * getFolder method on Store, Folder, or Session, or by invoking + * the list or listSubscribed methods + * on Folder. Folder objects returned by the above methods are not + * cached by the Store. Thus, invoking the getFolder method + * with the same folder name multiple times will return distinct Folder + * objects. Likewise for the list and listSubscribed + * methods.

+ * + * The Message objects within the Folder are cached by the Folder. + * Thus, invoking getMessage(msgno) on the same message number + * multiple times will return the same Message object, until an + * expunge is done on this Folder.

+ * + * Message objects from a Folder are only valid while a Folder is open + * and should not be accessed after the Folder is closed, even if the + * Folder is subsequently reopened. Instead, new Message objects must + * be fetched from the Folder after the Folder is reopened.

+ * + * Note that a Message's message number can change within a + * session if the containing Folder is expunged using the expunge + * method. Clients that use message numbers as references to messages + * should be aware of this and should be prepared to deal with this + * situation (probably by flushing out existing message number references + * and reloading them). Because of this complexity, it is better for + * clients to use Message objects as references to messages, rather than + * message numbers. Expunged Message objects still have to be + * pruned, but other Message objects in that folder are not affected by the + * expunge. + * + * @author John Mani + * @author Bill Shannon + */ + +public abstract class Folder implements AutoCloseable { + + /** + * The parent store. + */ + protected Store store; + + /** + * The open mode of this folder. The open mode is + * Folder.READ_ONLY, Folder.READ_WRITE, + * or -1 if not known. + * @since JavaMail 1.1 + */ + protected int mode = -1; + + /* + * The queue of events to be delivered. + */ + private final EventQueue q; + + /** + * Constructor that takes a Store object. + * + * @param store the Store that holds this folder + */ + protected Folder(Store store) { + this.store = store; + + // create or choose the appropriate event queue + Session session = store.getSession(); + String scope = + session.getProperties().getProperty("mail.event.scope", "folder"); + Executor executor = + (Executor)session.getProperties().get("mail.event.executor"); + if (scope.equalsIgnoreCase("application")) + q = EventQueue.getApplicationEventQueue(executor); + else if (scope.equalsIgnoreCase("session")) + q = session.getEventQueue(); + else if (scope.equalsIgnoreCase("store")) + q = store.getEventQueue(); + else // if (scope.equalsIgnoreCase("folder")) + q = new EventQueue(executor); + } + + /** + * Returns the name of this Folder.

+ * + * This method can be invoked on a closed Folder. + * + * @return name of the Folder + */ + public abstract String getName(); + + /** + * Returns the full name of this Folder. If the folder resides under + * the root hierarchy of this Store, the returned name is relative + * to the root. Otherwise an absolute name, starting with the + * hierarchy delimiter, is returned.

+ * + * This method can be invoked on a closed Folder. + * + * @return full name of the Folder + */ + public abstract String getFullName(); + + /** + * Return a URLName representing this folder. The returned URLName + * does not include the password used to access the store. + * + * @return the URLName representing this folder + * @exception MessagingException for failures + * @see URLName + * @since JavaMail 1.1 + */ + public URLName getURLName() throws MessagingException { + URLName storeURL = getStore().getURLName(); + String fullname = getFullName(); + StringBuilder encodedName = new StringBuilder(); + + if (fullname != null) { + /* + // We need to encode each of the folder's names. + char separator = getSeparator(); + StringTokenizer tok = new StringTokenizer( + fullname, new Character(separator).toString(), true); + + while (tok.hasMoreTokens()) { + String s = tok.nextToken(); + if (s.charAt(0) == separator) + encodedName.append(separator); + else + // XXX - should encode, but since there's no decoder... + //encodedName.append(java.net.URLEncoder.encode(s)); + encodedName.append(s); + } + */ + // append the whole thing, until we can encode + encodedName.append(fullname); + } + + /* + * Sure would be convenient if URLName had a + * constructor that took a base URLName. + */ + return new URLName(storeURL.getProtocol(), storeURL.getHost(), + storeURL.getPort(), encodedName.toString(), + storeURL.getUsername(), + null /* no password */); + } + + /** + * Returns the Store that owns this Folder object. + * This method can be invoked on a closed Folder. + * + * @return the Store + */ + public Store getStore() { + return store; + } + + /** + * Returns the parent folder of this folder. + * This method can be invoked on a closed Folder. If this folder + * is the top of a folder hierarchy, this method returns null.

+ * + * Note that since Folder objects are not cached, invoking this method + * returns a new distinct Folder object. + * + * @return Parent folder + * @exception MessagingException for failures + */ + public abstract Folder getParent() throws MessagingException; + + /** + * Tests if this folder physically exists on the Store. + * This method can be invoked on a closed Folder. + * + * @return true if the folder exists, otherwise false + * @see #create + * @exception MessagingException typically if the connection + * to the server is lost. + */ + public abstract boolean exists() throws MessagingException; + + /** + * Returns a list of Folders belonging to this Folder's namespace + * that match the specified pattern. Patterns may contain the wildcard + * characters "%", which matches any character except hierarchy + * delimiters, and "*", which matches any character.

+ * + * As an example, given the folder hierarchy:

+     *    Personal/
+     *       Finance/
+     *          Stocks
+     *          Bonus
+     *          StockOptions
+     *       Jokes
+     * 
+ * list("*") on "Personal" will return the whole + * hierarchy.
+ * list("%") on "Personal" will return "Finance" and + * "Jokes".
+ * list("Jokes") on "Personal" will return "Jokes".
+ * list("Stock*") on "Finance" will return "Stocks" + * and "StockOptions".

+ * + * Folder objects are not cached by the Store, so invoking this + * method on the same pattern multiple times will return that many + * distinct Folder objects.

+ * + * This method can be invoked on a closed Folder. + * + * @param pattern the match pattern + * @return array of matching Folder objects. An empty + * array is returned if no matching Folders exist. + * @see #listSubscribed + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MessagingException for other failures + */ + public abstract Folder[] list(String pattern) throws MessagingException; + + /** + * Returns a list of subscribed Folders belonging to this Folder's + * namespace that match the specified pattern. If the folder does + * not support subscription, this method should resolve to + * list. + * (The default implementation provided here, does just this). + * The pattern can contain wildcards as for list.

+ * + * Note that, at a given level of the folder hierarchy, a particular + * folder may not be subscribed, but folders underneath that folder + * in the folder hierarchy may be subscribed. In order to allow + * walking the folder hierarchy, such unsubscribed folders may be + * returned, indicating that a folder lower in the hierarchy is + * subscribed. The isSubscribed method on a folder will + * tell whether any particular folder is actually subscribed.

+ * + * Folder objects are not cached by the Store, so invoking this + * method on the same pattern multiple times will return that many + * distinct Folder objects.

+ * + * This method can be invoked on a closed Folder. + * + * @param pattern the match pattern + * @return array of matching subscribed Folder objects. An + * empty array is returned if no matching + * subscribed folders exist. + * @see #list + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MessagingException for other failures + */ + public Folder[] listSubscribed(String pattern) throws MessagingException { + return list(pattern); + } + + /** + * Convenience method that returns the list of folders under this + * Folder. This method just calls the list(String pattern) + * method with "%" as the match pattern. This method can + * be invoked on a closed Folder. + * + * @return array of Folder objects under this Folder. An + * empty array is returned if no subfolders exist. + * @see #list + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MessagingException for other failures + */ + + public Folder[] list() throws MessagingException { + return list("%"); + } + + /** + * Convenience method that returns the list of subscribed folders + * under this Folder. This method just calls the + * listSubscribed(String pattern) method with "%" + * as the match pattern. This method can be invoked on a closed Folder. + * + * @return array of subscribed Folder objects under this + * Folder. An empty array is returned if no subscribed + * subfolders exist. + * @see #listSubscribed + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MessagingException for other failures + */ + public Folder[] listSubscribed() throws MessagingException { + return listSubscribed("%"); + } + + /** + * Return the delimiter character that separates this Folder's pathname + * from the names of immediate subfolders. This method can be invoked + * on a closed Folder. + * + * @exception FolderNotFoundException if the implementation + * requires the folder to exist, but it does not + * @return Hierarchy separator character + */ + public abstract char getSeparator() throws MessagingException; + + /** + * This folder can contain messages + */ + public final static int HOLDS_MESSAGES = 0x01; + + /** + * This folder can contain other folders + */ + public final static int HOLDS_FOLDERS = 0x02; + + /** + * Returns the type of this Folder, that is, whether this folder can hold + * messages or subfolders or both. The returned value is an integer + * bitfield with the appropriate bits set. This method can be invoked + * on a closed folder. + * + * @return integer with appropriate bits set + * @exception FolderNotFoundException if this folder does + * not exist. + * @see #HOLDS_FOLDERS + * @see #HOLDS_MESSAGES + */ + public abstract int getType() throws MessagingException; + + /** + * Create this folder on the Store. When this folder is created, any + * folders in its path that do not exist are also created.

+ * + * If the creation is successful, a CREATED FolderEvent is delivered + * to any FolderListeners registered on this Folder and this Store. + * + * @param type The type of this folder. + * + * @return true if the creation succeeds, else false. + * @exception MessagingException for failures + * @see #HOLDS_FOLDERS + * @see #HOLDS_MESSAGES + * @see javax.mail.event.FolderEvent + */ + public abstract boolean create(int type) throws MessagingException; + + /** + * Returns true if this Folder is subscribed.

+ * + * This method can be invoked on a closed Folder.

+ * + * The default implementation provided here just returns true. + * + * @return true if this Folder is subscribed + */ + public boolean isSubscribed() { + return true; + } + + /** + * Subscribe or unsubscribe this Folder. Not all Stores support + * subscription.

+ * + * This method can be invoked on a closed Folder.

+ * + * The implementation provided here just throws the + * MethodNotSupportedException. + * + * @param subscribe true to subscribe, false to unsubscribe + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MethodNotSupportedException if this store + * does not support subscription + * @exception MessagingException for other failures + */ + public void setSubscribed(boolean subscribe) + throws MessagingException { + throw new MethodNotSupportedException(); + } + + /** + * Returns true if this Folder has new messages since the last time + * this indication was reset. When this indication is set or reset + * depends on the Folder implementation (and in the case of IMAP, + * depends on the server). This method can be used to implement + * a lightweight "check for new mail" operation on a Folder without + * opening it. (For example, a thread that monitors a mailbox and + * flags when it has new mail.) This method should indicate whether + * any messages in the Folder have the RECENT flag set.

+ * + * Note that this is not an incremental check for new mail, i.e., + * it cannot be used to determine whether any new messages have + * arrived since the last time this method was invoked. To + * implement incremental checks, the Folder needs to be opened.

+ * + * This method can be invoked on a closed Folder that can contain + * Messages. + * + * @return true if the Store has new Messages + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MessagingException for other failures + */ + public abstract boolean hasNewMessages() throws MessagingException; + + /** + * Return the Folder object corresponding to the given name. Note that + * this folder does not physically have to exist in the Store. The + * exists() method on a Folder indicates whether it really + * exists on the Store.

+ * + * In some Stores, name can be an absolute path if it starts with the + * hierarchy delimiter. Otherwise, it is interpreted relative to + * this Folder.

+ * + * Folder objects are not cached by the Store, so invoking this + * method on the same name multiple times will return that many + * distinct Folder objects.

+ * + * This method can be invoked on a closed Folder. + * + * @param name name of the Folder + * @return Folder object + * @exception MessagingException for failures + */ + public abstract Folder getFolder(String name) + throws MessagingException; + + /** + * Delete this Folder. This method will succeed only on a closed + * Folder.

+ * + * The recurse flag controls whether the deletion affects + * subfolders or not. If true, all subfolders are deleted, then this + * folder itself is deleted. If false, the behaviour is dependent on + * the folder type and is elaborated below: + * + *

    + *
  • + * The folder can contain only messages: (type == HOLDS_MESSAGES). + *
    + * All messages within the folder are removed. The folder + * itself is then removed. An appropriate FolderEvent is generated by + * the Store and this folder. + * + *
  • + * The folder can contain only subfolders: (type == HOLDS_FOLDERS). + *
    + * If this folder is empty (does not contain any + * subfolders at all), it is removed. An appropriate FolderEvent is + * generated by the Store and this folder.
    + * If this folder contains any subfolders, the delete fails + * and returns false. + * + *
  • + * The folder can contain subfolders as well as messages:
    + * If the folder is empty (no messages or subfolders), it + * is removed. If the folder contains no subfolders, but only messages, + * then all messages are removed. The folder itself is then removed. + * In both the above cases, an appropriate FolderEvent is + * generated by the Store and this folder.

    + * + * If the folder contains subfolders there are 3 possible + * choices an implementation is free to do: + * + *

      + *
    1. The operation fails, irrespective of whether this folder + * contains messages or not. Some implementations might elect to go + * with this simple approach. The delete() method returns false. + * + *
    2. Any messages within the folder are removed. Subfolders + * are not removed. The folder itself is not removed or affected + * in any manner. The delete() method returns true. And the + * exists() method on this folder will return true indicating that + * this folder still exists.
      + * An appropriate FolderEvent is generated by the Store and this folder. + * + *
    3. Any messages within the folder are removed. Subfolders are + * not removed. The folder itself changes its type from + * HOLDS_FOLDERS | HOLDS_MESSAGES to HOLDS_FOLDERS. Thus new + * messages cannot be added to this folder, but new subfolders can + * be created underneath. The delete() method returns true indicating + * success. The exists() method on this folder will return true + * indicating that this folder still exists.
      + * An appropriate FolderEvent is generated by the Store and this folder. + *
    + *
+ * + * @param recurse also delete subfolders? + * @return true if the Folder is deleted successfully + * @exception FolderNotFoundException if this folder does + * not exist + * @exception IllegalStateException if this folder is not in + * the closed state. + * @exception MessagingException for other failures + * @see javax.mail.event.FolderEvent + */ + public abstract boolean delete(boolean recurse) + throws MessagingException; + + /** + * Rename this Folder. This method will succeed only on a closed + * Folder.

+ * + * If the rename is successful, a RENAMED FolderEvent is delivered + * to FolderListeners registered on this folder and its containing + * Store. + * + * @param f a folder representing the new name for this Folder + * @return true if the Folder is renamed successfully + * @exception FolderNotFoundException if this folder does + * not exist + * @exception IllegalStateException if this folder is not in + * the closed state. + * @exception MessagingException for other failures + * @see javax.mail.event.FolderEvent + */ + public abstract boolean renameTo(Folder f) throws MessagingException; + + /** + * The Folder is read only. The state and contents of this + * folder cannot be modified. + */ + public static final int READ_ONLY = 1; + + /** + * The state and contents of this folder can be modified. + */ + public static final int READ_WRITE = 2; + + /** + * Open this Folder. This method is valid only on Folders that + * can contain Messages and that are closed.

+ * + * If this folder is opened successfully, an OPENED ConnectionEvent + * is delivered to any ConnectionListeners registered on this + * Folder.

+ * + * The effect of opening multiple connections to the same folder + * on a specifc Store is implementation dependent. Some implementations + * allow multiple readers, but only one writer. Others allow + * multiple writers as well as readers. + * + * @param mode open the Folder READ_ONLY or READ_WRITE + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception IllegalStateException if this folder is not in + * the closed state. + * @exception MessagingException for other failures + * @see #READ_ONLY + * @see #READ_WRITE + * @see #getType() + * @see javax.mail.event.ConnectionEvent + */ + public abstract void open(int mode) throws MessagingException; + + /** + * Close this Folder. This method is valid only on open Folders.

+ * + * A CLOSED ConnectionEvent is delivered to any ConnectionListeners + * registered on this Folder. Note that the folder is closed even + * if this method terminates abnormally by throwing a + * MessagingException. + * + * @param expunge expunges all deleted messages if this flag is true + * @exception IllegalStateException if this folder is not opened + * @exception MessagingException for other failures + * @see javax.mail.event.ConnectionEvent + */ + public abstract void close(boolean expunge) throws MessagingException; + + /** + * Close this Folder and expunge deleted messages.

+ * + * A CLOSED ConnectionEvent is delivered to any ConnectionListeners + * registered on this Folder. Note that the folder is closed even + * if this method terminates abnormally by throwing a + * MessagingException.

+ * + * This method supports the {@link java.lang.AutoCloseable AutoCloseable} + * interface.

+ * + * This implementation calls close(true). + * + * @exception IllegalStateException if this folder is not opened + * @exception MessagingException for other failures + * @see javax.mail.event.ConnectionEvent + * @since JavaMail 1.6 + */ + @Override + public void close() throws MessagingException { + close(true); + } + + /** + * Indicates whether this Folder is in the 'open' state. + * @return true if this Folder is in the 'open' state. + */ + public abstract boolean isOpen(); + + /** + * Return the open mode of this folder. Returns + * Folder.READ_ONLY, Folder.READ_WRITE, + * or -1 if the open mode is not known (usually only because an older + * Folder provider has not been updated to use this new + * method). + * + * @exception IllegalStateException if this folder is not opened + * @return the open mode of this folder + * @since JavaMail 1.1 + */ + public synchronized int getMode() { + if (!isOpen()) + throw new IllegalStateException("Folder not open"); + return mode; + } + + /** + * Get the permanent flags supported by this Folder. Returns a Flags + * object that contains all the flags supported.

+ * + * The special flag Flags.Flag.USER indicates that this Folder + * supports arbitrary user-defined flags.

+ * + * The supported permanent flags for a folder may not be available + * until the folder is opened. + * + * @return permanent flags, or null if not known + */ + public abstract Flags getPermanentFlags(); + + /** + * Get total number of messages in this Folder.

+ * + * This method can be invoked on a closed folder. However, note + * that for some folder implementations, getting the total message + * count can be an expensive operation involving actually opening + * the folder. In such cases, a provider can choose not to support + * this functionality in the closed state, in which case this method + * must return -1.

+ * + * Clients invoking this method on a closed folder must be aware + * that this is a potentially expensive operation. Clients must + * also be prepared to handle a return value of -1 in this case. + * + * @return total number of messages. -1 may be returned + * by certain implementations if this method is + * invoked on a closed folder. + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MessagingException for other failures + */ + public abstract int getMessageCount() throws MessagingException; + + /** + * Get the number of new messages in this Folder.

+ * + * This method can be invoked on a closed folder. However, note + * that for some folder implementations, getting the new message + * count can be an expensive operation involving actually opening + * the folder. In such cases, a provider can choose not to support + * this functionality in the closed state, in which case this method + * must return -1.

+ * + * Clients invoking this method on a closed folder must be aware + * that this is a potentially expensive operation. Clients must + * also be prepared to handle a return value of -1 in this case.

+ * + * This implementation returns -1 if this folder is closed. Else + * this implementation gets each Message in the folder using + * getMessage(int) and checks whether its + * RECENT flag is set. The total number of messages + * that have this flag set is returned. + * + * @return number of new messages. -1 may be returned + * by certain implementations if this method is + * invoked on a closed folder. + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MessagingException for other failures + */ + public synchronized int getNewMessageCount() + throws MessagingException { + if (!isOpen()) + return -1; + + int newmsgs = 0; + int total = getMessageCount(); + for (int i = 1; i <= total; i++) { + try { + if (getMessage(i).isSet(Flags.Flag.RECENT)) + newmsgs++; + } catch (MessageRemovedException me) { + // This is an expunged message, ignore it. + continue; + } + } + return newmsgs; + } + + /** + * Get the total number of unread messages in this Folder.

+ * + * This method can be invoked on a closed folder. However, note + * that for some folder implementations, getting the unread message + * count can be an expensive operation involving actually opening + * the folder. In such cases, a provider can choose not to support + * this functionality in the closed state, in which case this method + * must return -1.

+ * + * Clients invoking this method on a closed folder must be aware + * that this is a potentially expensive operation. Clients must + * also be prepared to handle a return value of -1 in this case.

+ * + * This implementation returns -1 if this folder is closed. Else + * this implementation gets each Message in the folder using + * getMessage(int) and checks whether its + * SEEN flag is set. The total number of messages + * that do not have this flag set is returned. + * + * @return total number of unread messages. -1 may be returned + * by certain implementations if this method is + * invoked on a closed folder. + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MessagingException for other failures + */ + public synchronized int getUnreadMessageCount() + throws MessagingException { + if (!isOpen()) + return -1; + + int unread = 0; + int total = getMessageCount(); + for (int i = 1; i <= total; i++) { + try { + if (!getMessage(i).isSet(Flags.Flag.SEEN)) + unread++; + } catch (MessageRemovedException me) { + // This is an expunged message, ignore it. + continue; + } + } + return unread; + } + + /** + * Get the number of deleted messages in this Folder.

+ * + * This method can be invoked on a closed folder. However, note + * that for some folder implementations, getting the deleted message + * count can be an expensive operation involving actually opening + * the folder. In such cases, a provider can choose not to support + * this functionality in the closed state, in which case this method + * must return -1.

+ * + * Clients invoking this method on a closed folder must be aware + * that this is a potentially expensive operation. Clients must + * also be prepared to handle a return value of -1 in this case.

+ * + * This implementation returns -1 if this folder is closed. Else + * this implementation gets each Message in the folder using + * getMessage(int) and checks whether its + * DELETED flag is set. The total number of messages + * that have this flag set is returned. + * + * @return number of deleted messages. -1 may be returned + * by certain implementations if this method is + * invoked on a closed folder. + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MessagingException for other failures + * @since JavaMail 1.3 + */ + public synchronized int getDeletedMessageCount() throws MessagingException { + if (!isOpen()) + return -1; + + int deleted = 0; + int total = getMessageCount(); + for (int i = 1; i <= total; i++) { + try { + if (getMessage(i).isSet(Flags.Flag.DELETED)) + deleted++; + } catch (MessageRemovedException me) { + // This is an expunged message, ignore it. + continue; + } + } + return deleted; + } + + /** + * Get the Message object corresponding to the given message + * number. A Message object's message number is the relative + * position of this Message in its Folder. Messages are numbered + * starting at 1 through the total number of message in the folder. + * Note that the message number for a particular Message can change + * during a session if other messages in the Folder are deleted and + * the Folder is expunged.

+ * + * Message objects are light-weight references to the actual message + * that get filled up on demand. Hence Folder implementations are + * expected to provide light-weight Message objects.

+ * + * Unlike Folder objects, repeated calls to getMessage with the + * same message number will return the same Message object, as + * long as no messages in this folder have been expunged.

+ * + * Since message numbers can change within a session if the folder + * is expunged , clients are advised not to use message numbers as + * references to messages. Use Message objects instead. + * + * @param msgnum the message number + * @return the Message object + * @see #getMessageCount + * @see #fetch + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception IllegalStateException if this folder is not opened + * @exception IndexOutOfBoundsException if the message number + * is out of range. + * @exception MessagingException for other failures + */ + public abstract Message getMessage(int msgnum) + throws MessagingException; + + /** + * Get the Message objects for message numbers ranging from start + * through end, both start and end inclusive. Note that message + * numbers start at 1, not 0.

+ * + * Message objects are light-weight references to the actual message + * that get filled up on demand. Hence Folder implementations are + * expected to provide light-weight Message objects.

+ * + * This implementation uses getMessage(index) to obtain the required + * Message objects. Note that the returned array must contain + * (end-start+1) Message objects. + * + * @param start the number of the first message + * @param end the number of the last message + * @return the Message objects + * @see #fetch + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception IllegalStateException if this folder is not opened. + * @exception IndexOutOfBoundsException if the start or end + * message numbers are out of range. + * @exception MessagingException for other failures + */ + public synchronized Message[] getMessages(int start, int end) + throws MessagingException { + Message[] msgs = new Message[end - start +1]; + for (int i = start; i <= end; i++) + msgs[i - start] = getMessage(i); + return msgs; + } + + /** + * Get the Message objects for message numbers specified in + * the array.

+ * + * Message objects are light-weight references to the actual message + * that get filled up on demand. Hence Folder implementations are + * expected to provide light-weight Message objects.

+ * + * This implementation uses getMessage(index) to obtain the required + * Message objects. Note that the returned array must contain + * msgnums.length Message objects + * + * @param msgnums the array of message numbers + * @return the array of Message objects. + * @see #fetch + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception IllegalStateException if this folder is not opened. + * @exception IndexOutOfBoundsException if any message number + * in the given array is out of range. + * @exception MessagingException for other failures + */ + public synchronized Message[] getMessages(int[] msgnums) + throws MessagingException { + int len = msgnums.length; + Message[] msgs = new Message[len]; + for (int i = 0; i < len; i++) + msgs[i] = getMessage(msgnums[i]); + return msgs; + } + + /** + * Get all Message objects from this Folder. Returns an empty array + * if the folder is empty. + * + * Clients can use Message objects (instead of sequence numbers) + * as references to the messages within a folder; this method supplies + * the Message objects to the client. Folder implementations are + * expected to provide light-weight Message objects, which get + * filled on demand.

+ * + * This implementation invokes getMessageCount() to get + * the current message count and then uses getMessage() + * to get Message objects from 1 till the message count. + * + * @return array of Message objects, empty array if folder + * is empty. + * @see #fetch + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception IllegalStateException if this folder is not opened. + * @exception MessagingException for other failures + */ + public synchronized Message[] getMessages() throws MessagingException { + if (!isOpen()) // otherwise getMessageCount might return -1 + throw new IllegalStateException("Folder not open"); + int total = getMessageCount(); + Message[] msgs = new Message[total]; + for (int i = 1; i <= total; i++) + msgs[i-1] = getMessage(i); + return msgs; + } + + /** + * Append given Messages to this folder. This method can be + * invoked on a closed Folder. An appropriate MessageCountEvent + * is delivered to any MessageCountListener registered on this + * folder when the messages arrive in the folder.

+ * + * Folder implementations must not abort this operation if a + * Message in the given message array turns out to be an + * expunged Message. + * + * @param msgs array of Messages to be appended + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception MessagingException if the append failed. + */ + public abstract void appendMessages(Message[] msgs) + throws MessagingException; + + /** + * Prefetch the items specified in the FetchProfile for the + * given Messages.

+ * + * Clients use this method to indicate that the specified items are + * needed en-masse for the given message range. Implementations are + * expected to retrieve these items for the given message range in + * a efficient manner. Note that this method is just a hint to the + * implementation to prefetch the desired items.

+ * + * An example is a client filling its header-view window with + * the Subject, From and X-mailer headers for all messages in the + * folder. + *

+     *
+     *  Message[] msgs = folder.getMessages();
+     *
+     *  FetchProfile fp = new FetchProfile();
+     *  fp.add(FetchProfile.Item.ENVELOPE);
+     *  fp.add("X-mailer");
+     *  folder.fetch(msgs, fp);
+     *  
+     *  for (int i = 0; i < folder.getMessageCount(); i++) {
+     *      display(msg[i].getFrom());
+     *      display(msg[i].getSubject());
+     *      display(msg[i].getHeader("X-mailer"));
+     *  }
+     *
+     * 

+ * + * The implementation provided here just returns without + * doing anything useful. Providers wanting to provide a real + * implementation for this method should override this method. + * + * @param msgs fetch items for these messages + * @param fp the FetchProfile + * @exception IllegalStateException if this folder is not opened + * @exception MessagingException for other failures + */ + public void fetch(Message[] msgs, FetchProfile fp) + throws MessagingException { + return; + } + + /** + * Set the specified flags on the messages specified in the array. + * This will result in appropriate MessageChangedEvents being + * delivered to any MessageChangedListener registered on this + * Message's containing folder.

+ * + * Note that the specified Message objects must + * belong to this folder. Certain Folder implementations can + * optimize the operation of setting Flags for a group of messages, + * so clients might want to use this method, rather than invoking + * Message.setFlags for each Message.

+ * + * This implementation degenerates to invoking setFlags() + * on each Message object. Specific Folder implementations that can + * optimize this case should do so. + * Also, an implementation must not abort the operation if a Message + * in the array turns out to be an expunged Message. + * + * @param msgs the array of message objects + * @param flag Flags object containing the flags to be set + * @param value set the flags to this boolean value + * @exception IllegalStateException if this folder is not opened + * or if it has been opened READ_ONLY. + * @exception MessagingException for other failures + * @see Message#setFlags + * @see javax.mail.event.MessageChangedEvent + */ + public synchronized void setFlags(Message[] msgs, + Flags flag, boolean value) throws MessagingException { + for (int i = 0; i < msgs.length; i++) { + try { + msgs[i].setFlags(flag, value); + } catch (MessageRemovedException me) { + // This message is expunged, skip + } + } + } + + /** + * Set the specified flags on the messages numbered from start + * through end, both start and end inclusive. Note that message + * numbers start at 1, not 0. + * This will result in appropriate MessageChangedEvents being + * delivered to any MessageChangedListener registered on this + * Message's containing folder.

+ * + * Certain Folder implementations can + * optimize the operation of setting Flags for a group of messages, + * so clients might want to use this method, rather than invoking + * Message.setFlags for each Message.

+ * + * The default implementation uses getMessage(int) to + * get each Message object and then invokes + * setFlags on that object to set the flags. + * Specific Folder implementations that can optimize this case should do so. + * Also, an implementation must not abort the operation if a message + * number refers to an expunged message. + * + * @param start the number of the first message + * @param end the number of the last message + * @param flag Flags object containing the flags to be set + * @param value set the flags to this boolean value + * @exception IllegalStateException if this folder is not opened + * or if it has been opened READ_ONLY. + * @exception IndexOutOfBoundsException if the start or end + * message numbers are out of range. + * @exception MessagingException for other failures + * @see Message#setFlags + * @see javax.mail.event.MessageChangedEvent + */ + public synchronized void setFlags(int start, int end, + Flags flag, boolean value) throws MessagingException { + for (int i = start; i <= end; i++) { + try { + Message msg = getMessage(i); + msg.setFlags(flag, value); + } catch (MessageRemovedException me) { + // This message is expunged, skip + } + } + } + + /** + * Set the specified flags on the messages whose message numbers + * are in the array. + * This will result in appropriate MessageChangedEvents being + * delivered to any MessageChangedListener registered on this + * Message's containing folder.

+ * + * Certain Folder implementations can + * optimize the operation of setting Flags for a group of messages, + * so clients might want to use this method, rather than invoking + * Message.setFlags for each Message.

+ * + * The default implementation uses getMessage(int) to + * get each Message object and then invokes + * setFlags on that object to set the flags. + * Specific Folder implementations that can optimize this case should do so. + * Also, an implementation must not abort the operation if a message + * number refers to an expunged message. + * + * @param msgnums the array of message numbers + * @param flag Flags object containing the flags to be set + * @param value set the flags to this boolean value + * @exception IllegalStateException if this folder is not opened + * or if it has been opened READ_ONLY. + * @exception IndexOutOfBoundsException if any message number + * in the given array is out of range. + * @exception MessagingException for other failures + * @see Message#setFlags + * @see javax.mail.event.MessageChangedEvent + */ + public synchronized void setFlags(int[] msgnums, + Flags flag, boolean value) throws MessagingException { + for (int i = 0; i < msgnums.length; i++) { + try { + Message msg = getMessage(msgnums[i]); + msg.setFlags(flag, value); + } catch (MessageRemovedException me) { + // This message is expunged, skip + } + } + } + + /** + * Copy the specified Messages from this Folder into another + * Folder. This operation appends these Messages to the + * destination Folder. The destination Folder does not have to + * be opened. An appropriate MessageCountEvent + * is delivered to any MessageCountListener registered on the + * destination folder when the messages arrive in the folder.

+ * + * Note that the specified Message objects must + * belong to this folder. Folder implementations might be able + * to optimize this method by doing server-side copies.

+ * + * This implementation just invokes appendMessages() + * on the destination folder to append the given Messages. Specific + * folder implementations that support server-side copies should + * do so, if the destination folder's Store is the same as this + * folder's Store. + * Also, an implementation must not abort the operation if a + * Message in the array turns out to be an expunged Message. + * + * @param msgs the array of message objects + * @param folder the folder to copy the messages to + * @exception FolderNotFoundException if the destination + * folder does not exist. + * @exception IllegalStateException if this folder is not opened. + * @exception MessagingException for other failures + * @see #appendMessages + */ + public void copyMessages(Message[] msgs, Folder folder) + throws MessagingException { + if (!folder.exists()) + throw new FolderNotFoundException( + folder.getFullName() + " does not exist", + folder); + + folder.appendMessages(msgs); + } + + /** + * Expunge (permanently remove) messages marked DELETED. Returns an + * array containing the expunged message objects. The + * getMessageNumber method + * on each of these message objects returns that Message's original + * (that is, prior to the expunge) sequence number. A MessageCountEvent + * containing the expunged messages is delivered to any + * MessageCountListeners registered on the folder.

+ * + * Expunge causes the renumbering of Message objects subsequent to + * the expunged messages. Clients that use message numbers as + * references to messages should be aware of this and should be + * prepared to deal with the situation (probably by flushing out + * existing message number caches and reloading them). Because of + * this complexity, it is better for clients to use Message objects + * as references to messages, rather than message numbers. Any + * expunged Messages objects still have to be pruned, but other + * Messages in that folder are not affected by the expunge.

+ * + * After a message is expunged, only the isExpunged and + * getMessageNumber methods are still valid on the + * corresponding Message object; other methods may throw + * MessageRemovedException + * + * @return array of expunged Message objects + * @exception FolderNotFoundException if this folder does not + * exist + * @exception IllegalStateException if this folder is not opened. + * @exception MessagingException for other failures + * @see Message#isExpunged + * @see javax.mail.event.MessageCountEvent + */ + public abstract Message[] expunge() throws MessagingException; + + /** + * Search this Folder for messages matching the specified + * search criterion. Returns an array containing the matching + * messages . Returns an empty array if no matches were found.

+ * + * This implementation invokes + * search(term, getMessages()), to apply the search + * over all the messages in this folder. Providers that can implement + * server-side searching might want to override this method to provide + * a more efficient implementation. + * + * @param term the search criterion + * @return array of matching messages + * @exception javax.mail.search.SearchException if the search + * term is too complex for the implementation to handle. + * @exception FolderNotFoundException if this folder does + * not exist. + * @exception IllegalStateException if this folder is not opened. + * @exception MessagingException for other failures + * @see javax.mail.search.SearchTerm + */ + public Message[] search(SearchTerm term) throws MessagingException { + return search(term, getMessages()); + } + + /** + * Search the given array of messages for those that match the + * specified search criterion. Returns an array containing the + * matching messages. Returns an empty array if no matches were + * found.

+ * + * Note that the specified Message objects must + * belong to this folder.

+ * + * This implementation iterates through the given array of messages, + * and applies the search criterion on each message by calling + * its match() method with the given term. The + * messages that succeed in the match are returned. Providers + * that can implement server-side searching might want to override + * this method to provide a more efficient implementation. If the + * search term is too complex or contains user-defined terms that + * cannot be executed on the server, providers may elect to either + * throw a SearchException or degenerate to client-side searching by + * calling super.search() to invoke this implementation. + * + * @param term the search criterion + * @param msgs the messages to be searched + * @return array of matching messages + * @exception javax.mail.search.SearchException if the search + * term is too complex for the implementation to handle. + * @exception IllegalStateException if this folder is not opened + * @exception MessagingException for other failures + * @see javax.mail.search.SearchTerm + */ + public Message[] search(SearchTerm term, Message[] msgs) + throws MessagingException { + List matchedMsgs = new ArrayList<>(); + + // Run thru the given messages + for (Message msg : msgs) { + try { + if (msg.match(term)) // matched + matchedMsgs.add(msg); // add it + } catch(MessageRemovedException mrex) { } + } + + return matchedMsgs.toArray(new Message[matchedMsgs.size()]); + } + + /* + * The set of listeners are stored in Vectors appropriate to their + * type. We mark all listener Vectors as "volatile" because, while + * we initialize them inside this folder's synchronization lock, + * they are accessed (checked for null) in the "notify" methods, + * which can't be synchronized due to lock ordering constraints. + * Since the listener fields (the handles on the Vector objects) + * are only ever set, and are never cleared, we believe this is + * safe. The code that dispatches the notifications will either + * see the null and assume there are no listeners or will see the + * Vector and will process the listeners. There's an inherent race + * between adding a listener and notifying the listeners; the lack + * of synchronization during notification does not make the race + * condition significantly worse. If one thread is setting a + * listener at the "same" time an event is being dispatched, the + * dispatch code might not see the listener right away. The + * dispatch code doesn't have to worry about the Vector handle + * being set to null, and thus using an out-of-date set of + * listeners, because we never set the field to null. + */ + + // Vector of connection listeners. + private volatile Vector connectionListeners = null; + + /** + * Add a listener for Connection events on this Folder.

+ * + * The implementation provided here adds this listener + * to an internal list of ConnectionListeners. + * + * @param l the Listener for Connection events + * @see javax.mail.event.ConnectionEvent + */ + public synchronized void + addConnectionListener(ConnectionListener l) { + if (connectionListeners == null) + connectionListeners = new Vector<>(); + connectionListeners.addElement(l); + } + + /** + * Remove a Connection event listener.

+ * + * The implementation provided here removes this listener + * from the internal list of ConnectionListeners. + * + * @param l the listener + * @see #addConnectionListener + */ + public synchronized void + removeConnectionListener(ConnectionListener l) { + if (connectionListeners != null) + connectionListeners.removeElement(l); + } + + /** + * Notify all ConnectionListeners. Folder implementations are + * expected to use this method to broadcast connection events.

+ * + * The provided implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * ConnectionListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param type the ConnectionEvent type + * @see javax.mail.event.ConnectionEvent + */ + protected void notifyConnectionListeners(int type) { + if (connectionListeners != null) { + ConnectionEvent e = new ConnectionEvent(this, type); + queueEvent(e, connectionListeners); + } + + /* Fix for broken JDK1.1.x Garbage collector : + * The 'conservative' GC in JDK1.1.x occasionally fails to + * garbage-collect Threads which are in the wait state. + * This would result in thread (and consequently memory) leaks. + * + * We attempt to fix this by sending a 'terminator' event + * to the queue, after we've sent the CLOSED event. The + * terminator event causes the event-dispatching thread to + * self destruct. + */ + if (type == ConnectionEvent.CLOSED) + q.terminateQueue(); + } + + // Vector of folder listeners + private volatile Vector folderListeners = null; + + /** + * Add a listener for Folder events on this Folder.

+ * + * The implementation provided here adds this listener + * to an internal list of FolderListeners. + * + * @param l the Listener for Folder events + * @see javax.mail.event.FolderEvent + */ + public synchronized void addFolderListener(FolderListener l) { + if (folderListeners == null) + folderListeners = new Vector<>(); + folderListeners.addElement(l); + } + + /** + * Remove a Folder event listener.

+ * + * The implementation provided here removes this listener + * from the internal list of FolderListeners. + * + * @param l the listener + * @see #addFolderListener + */ + public synchronized void removeFolderListener(FolderListener l) { + if (folderListeners != null) + folderListeners.removeElement(l); + } + + /** + * Notify all FolderListeners registered on this Folder and + * this folder's Store. Folder implementations are expected + * to use this method to broadcast Folder events.

+ * + * The implementation provided here queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the + * FolderListeners registered on this folder. The implementation + * also invokes notifyFolderListeners on this folder's + * Store to notify any FolderListeners registered on the store. + * + * @param type type of FolderEvent + * @see #notifyFolderRenamedListeners + */ + protected void notifyFolderListeners(int type) { + if (folderListeners != null) { + FolderEvent e = new FolderEvent(this, this, type); + queueEvent(e, folderListeners); + } + store.notifyFolderListeners(type, this); + } + + /** + * Notify all FolderListeners registered on this Folder and + * this folder's Store about the renaming of this folder. + * Folder implementations are expected to use this method to + * broadcast Folder events indicating the renaming of folders.

+ * + * The implementation provided here queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the + * FolderListeners registered on this folder. The implementation + * also invokes notifyFolderRenamedListeners on this + * folder's Store to notify any FolderListeners registered on the store. + * + * @param folder Folder representing the new name. + * @see #notifyFolderListeners + * @since JavaMail 1.1 + */ + protected void notifyFolderRenamedListeners(Folder folder) { + if (folderListeners != null) { + FolderEvent e = new FolderEvent(this, this, folder, + FolderEvent.RENAMED); + queueEvent(e, folderListeners); + } + store.notifyFolderRenamedListeners(this, folder); + } + + // Vector of MessageCount listeners + private volatile Vector messageCountListeners = null; + + /** + * Add a listener for MessageCount events on this Folder.

+ * + * The implementation provided here adds this listener + * to an internal list of MessageCountListeners. + * + * @param l the Listener for MessageCount events + * @see javax.mail.event.MessageCountEvent + */ + public synchronized void addMessageCountListener(MessageCountListener l) { + if (messageCountListeners == null) + messageCountListeners = new Vector<>(); + messageCountListeners.addElement(l); + } + + /** + * Remove a MessageCount listener.

+ * + * The implementation provided here removes this listener + * from the internal list of MessageCountListeners. + * + * @param l the listener + * @see #addMessageCountListener + */ + public synchronized void + removeMessageCountListener(MessageCountListener l) { + if (messageCountListeners != null) + messageCountListeners.removeElement(l); + } + + /** + * Notify all MessageCountListeners about the addition of messages + * into this folder. Folder implementations are expected to use this + * method to broadcast MessageCount events for indicating arrival of + * new messages.

+ * + * The provided implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * MessageCountListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param msgs the messages that were added + */ + protected void notifyMessageAddedListeners(Message[] msgs) { + if (messageCountListeners == null) + return; + + MessageCountEvent e = new MessageCountEvent( + this, + MessageCountEvent.ADDED, + false, + msgs); + + queueEvent(e, messageCountListeners); + } + + /** + * Notify all MessageCountListeners about the removal of messages + * from this Folder. Folder implementations are expected to use this + * method to broadcast MessageCount events indicating removal of + * messages.

+ * + * The provided implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * MessageCountListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param removed was the message removed by this client? + * @param msgs the messages that were removed + */ + protected void notifyMessageRemovedListeners(boolean removed, + Message[] msgs) { + if (messageCountListeners == null) + return; + + MessageCountEvent e = new MessageCountEvent( + this, + MessageCountEvent.REMOVED, + removed, + msgs); + queueEvent(e, messageCountListeners); + } + + // Vector of MessageChanged listeners. + private volatile Vector messageChangedListeners + = null; + + /** + * Add a listener for MessageChanged events on this Folder.

+ * + * The implementation provided here adds this listener + * to an internal list of MessageChangedListeners. + * + * @param l the Listener for MessageChanged events + * @see javax.mail.event.MessageChangedEvent + */ + public synchronized void + addMessageChangedListener(MessageChangedListener l) { + if (messageChangedListeners == null) + messageChangedListeners = new Vector<>(); + messageChangedListeners.addElement(l); + } + + /** + * Remove a MessageChanged listener.

+ * + * The implementation provided here removes this listener + * from the internal list of MessageChangedListeners. + * + * @param l the listener + * @see #addMessageChangedListener + */ + public synchronized void + removeMessageChangedListener(MessageChangedListener l) { + if (messageChangedListeners != null) + messageChangedListeners.removeElement(l); + } + + /** + * Notify all MessageChangedListeners. Folder implementations are + * expected to use this method to broadcast MessageChanged events.

+ * + * The provided implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to registered + * MessageChangedListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param type the MessageChangedEvent type + * @param msg the message that changed + */ + protected void notifyMessageChangedListeners(int type, Message msg) { + if (messageChangedListeners == null) + return; + + MessageChangedEvent e = new MessageChangedEvent(this, type, msg); + queueEvent(e, messageChangedListeners); + } + + /* + * Add the event and vector of listeners to the queue to be delivered. + */ + @SuppressWarnings("unchecked") + private void queueEvent(MailEvent event, + Vector vector) { + /* + * Copy the vector in order to freeze the state of the set + * of EventListeners the event should be delivered to prior + * to delivery. This ensures that any changes made to the + * Vector from a target listener's method during the delivery + * of this event will not take effect until after the event is + * delivered. + */ + Vector v = (Vector)vector.clone(); + q.enqueue(event, v); + } + + @Override + protected void finalize() throws Throwable { + try { + q.terminateQueue(); + } finally { + super.finalize(); + } + } + + /** + * override the default toString(), it will return the String + * from Folder.getFullName() or if that is null, it will use + * the default toString() behavior. + */ + + @Override + public String toString() { + String s = getFullName(); + if (s != null) + return s; + else + return super.toString(); + } +} diff --git a/app/src/main/java/javax/mail/FolderClosedException.java b/app/src/main/java/javax/mail/FolderClosedException.java new file mode 100644 index 0000000000..7e8a4b4830 --- /dev/null +++ b/app/src/main/java/javax/mail/FolderClosedException.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * This exception is thrown when a method is invoked on a Messaging object + * and the Folder that owns that object has died due to some reason.

+ * + * Following the exception, the Folder is reset to the "closed" state. + * All messaging objects owned by the Folder should be considered invalid. + * The Folder can be reopened using the "open" method to reestablish the + * lost connection.

+ * + * The getMessage() method returns more detailed information about the + * error that caused this exception.

+ * + * @author John Mani + */ + +public class FolderClosedException extends MessagingException { + transient private Folder folder; + + private static final long serialVersionUID = 1687879213433302315L; + + /** + * Constructs a FolderClosedException. + * + * @param folder The Folder + */ + public FolderClosedException(Folder folder) { + this(folder, null); + } + + /** + * Constructs a FolderClosedException with the specified + * detail message. + * + * @param folder The Folder + * @param message The detailed error message + */ + public FolderClosedException(Folder folder, String message) { + super(message); + this.folder = folder; + } + + /** + * Constructs a FolderClosedException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param folder The Folder + * @param message The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public FolderClosedException(Folder folder, String message, Exception e) { + super(message, e); + this.folder = folder; + } + + /** + * Returns the dead Folder object + * + * @return the dead Folder object + */ + public Folder getFolder() { + return folder; + } +} diff --git a/app/src/main/java/javax/mail/FolderNotFoundException.java b/app/src/main/java/javax/mail/FolderNotFoundException.java new file mode 100644 index 0000000000..0ea6745e05 --- /dev/null +++ b/app/src/main/java/javax/mail/FolderNotFoundException.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.lang.*; + +/** + * This exception is thrown by Folder methods, when those + * methods are invoked on a non existent folder. + * + * @author John Mani + */ + +public class FolderNotFoundException extends MessagingException { + transient private Folder folder; + + private static final long serialVersionUID = 472612108891249403L; + + /** + * Constructs a FolderNotFoundException with no detail message. + */ + public FolderNotFoundException() { + super(); + } + + /** + * Constructs a FolderNotFoundException. + * + * @param folder The Folder + * @since JavaMail 1.2 + */ + public FolderNotFoundException(Folder folder) { + super(); + this.folder = folder; + } + + /** + * Constructs a FolderNotFoundException with the specified + * detail message. + * + * @param folder The Folder + * @param s The detailed error message + * @since JavaMail 1.2 + */ + public FolderNotFoundException(Folder folder, String s) { + super(s); + this.folder = folder; + } + + /** + * Constructs a FolderNotFoundException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param folder The Folder + * @param s The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public FolderNotFoundException(Folder folder, String s, Exception e) { + super(s, e); + this.folder = folder; + } + + /** + * Constructs a FolderNotFoundException with the specified detail message + * and the specified folder. + * + * @param s The detail message + * @param folder The Folder + */ + public FolderNotFoundException(String s, Folder folder) { + super(s); + this.folder = folder; + } + + /** + * Returns the offending Folder object. + * + * @return the Folder object. Note that the returned value can be + * null. + */ + public Folder getFolder() { + return folder; + } +} diff --git a/app/src/main/java/javax/mail/Header.java b/app/src/main/java/javax/mail/Header.java new file mode 100644 index 0000000000..924f84cb18 --- /dev/null +++ b/app/src/main/java/javax/mail/Header.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + + +/** + * The Header class stores a name/value pair to represent headers. + * + * @author John Mani + */ + +public class Header { + + /** + * The name of the header. + * + * @since JavaMail 1.4 + */ + protected String name; + + /** + * The value of the header. + * + * @since JavaMail 1.4 + */ + protected String value; + + /** + * Construct a Header object. + * + * @param name name of the header + * @param value value of the header + */ + public Header(String name, String value) { + this.name = name; + this.value = value; + } + + /** + * Returns the name of this header. + * + * @return name of the header + */ + public String getName() { + return name; + } + + /** + * Returns the value of this header. + * + * @return value of the header + */ + public String getValue() { + return value; + } +} diff --git a/app/src/main/java/javax/mail/IllegalWriteException.java b/app/src/main/java/javax/mail/IllegalWriteException.java new file mode 100644 index 0000000000..8011c4c3f5 --- /dev/null +++ b/app/src/main/java/javax/mail/IllegalWriteException.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + + +/** + * The exception thrown when a write is attempted on a read-only attribute + * of any Messaging object. + * + * @author John Mani + */ + +public class IllegalWriteException extends MessagingException { + + private static final long serialVersionUID = 3974370223328268013L; + + /** + * Constructs an IllegalWriteException with no detail message. + */ + public IllegalWriteException() { + super(); + } + + /** + * Constructs an IllegalWriteException with the specified + * detail message. + * + * @param s The detailed error message + */ + public IllegalWriteException(String s) { + super(s); + } + + /** + * Constructs an IllegalWriteException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param s The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public IllegalWriteException(String s, Exception e) { + super(s, e); + } +} diff --git a/app/src/main/java/javax/mail/MailSessionDefinition.java b/app/src/main/java/javax/mail/MailSessionDefinition.java new file mode 100644 index 0000000000..933574dead --- /dev/null +++ b/app/src/main/java/javax/mail/MailSessionDefinition.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.Repeatable; + +/** + * Annotation used by Jakarta EE applications to define a MailSession + * to be registered with JNDI. The MailSession may be configured + * by setting the annotation elements for commonly used Session + * properties. Additional standard and vendor-specific properties may be + * specified using the properties element. + *

+ * The session will be registered under the name specified in the + * name element. It may be defined to be in any valid + * Jakarta EE namespace, and will determine the accessibility of + * the session from other components. + * + * @since JavaMail 1.5 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(MailSessionDefinitions.class) +public @interface MailSessionDefinition { + + /** + * Description of this mail session. + * + * @return the description + */ + String description() default ""; + + /** + * JNDI name by which the mail session will be registered. + * + * @return the JNDI name + */ + String name(); + + /** + * Store protocol name. + * + * @return the store protocol name + */ + String storeProtocol() default ""; + + /** + * Transport protocol name. + * + * @return the transport protocol name + */ + String transportProtocol() default ""; + + /** + * Host name for the mail server. + * + * @return the host name + */ + String host() default ""; + + /** + * User name to use for authentication. + * + * @return the user name + */ + String user() default ""; + + /** + * Password to use for authentication. + * + * @return the password + */ + String password() default ""; + + /** + * From address for the user. + * + * @return the from address + */ + String from() default ""; + + /** + * Properties to include in the Session. + * Properties are specified using the format: + * propertyName=propertyValue with one property per array element. + * + * @return the properties + */ + String[] properties() default {}; +} diff --git a/app/src/main/java/javax/mail/MailSessionDefinitions.java b/app/src/main/java/javax/mail/MailSessionDefinitions.java new file mode 100644 index 0000000000..b97462b107 --- /dev/null +++ b/app/src/main/java/javax/mail/MailSessionDefinitions.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.lang.annotation.Target; +import java.lang.annotation.Retention; +import java.lang.annotation.ElementType; +import java.lang.annotation.RetentionPolicy; + +/** + * Declares one or more MailSessionDefinition annotations. + * + * @see MailSessionDefinition + * @since JavaMail 1.5 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface MailSessionDefinitions { + MailSessionDefinition[] value(); +} diff --git a/app/src/main/java/javax/mail/Message.java b/app/src/main/java/javax/mail/Message.java new file mode 100644 index 0000000000..370fd28103 --- /dev/null +++ b/app/src/main/java/javax/mail/Message.java @@ -0,0 +1,704 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.util.Date; +import java.io.*; +import javax.mail.search.SearchTerm; + +/** + * This class models an email message. This is an abstract class. + * Subclasses provide actual implementations.

+ * + * Message implements the Part interface. Message contains a set of + * attributes and a "content". Messages within a folder also have a + * set of flags that describe its state within the folder.

+ * + * Message defines some new attributes in addition to those defined + * in the Part interface. These attributes specify meta-data + * for the message - i.e., addressing and descriptive information about + * the message.

+ * + * Message objects are obtained either from a Folder or by constructing + * a new Message object of the appropriate subclass. Messages that have + * been received are normally retrieved from a folder named "INBOX".

+ * + * A Message object obtained from a folder is just a lightweight + * reference to the actual message. The Message is 'lazily' filled + * up (on demand) when each item is requested from the message. Note + * that certain folder implementations may return Message objects that + * are pre-filled with certain user-specified items. + + * To send a message, an appropriate subclass of Message (e.g., + * MimeMessage) is instantiated, the attributes and content are + * filled in, and the message is sent using the Transport.send + * method.

+ * + * @author John Mani + * @author Bill Shannon + * @author Max Spivak + * @see javax.mail.Part + */ + +public abstract class Message implements Part { + + /** + * The number of this message within its folder, or zero if + * the message was not retrieved from a folder. + */ + protected int msgnum = 0; + + /** + * True if this message has been expunged. + */ + protected boolean expunged = false; + + /** + * The containing folder, if this message is obtained from a folder + */ + protected Folder folder = null; + + /** + * The Session object for this Message + */ + protected Session session = null; + + /** + * No-arg version of the constructor. + */ + protected Message() { } + + /** + * Constructor that takes a Folder and a message number. + * Used by Folder implementations. + * + * @param folder containing folder + * @param msgnum this message's sequence number within this folder + */ + protected Message(Folder folder, int msgnum) { + this.folder = folder; + this.msgnum = msgnum; + session = folder.store.session; + } + + /** + * Constructor that takes a Session. Used for client created + * Message objects. + * + * @param session A Session object + */ + protected Message(Session session) { + this.session = session; + } + + /** + * Return the Session used when this message was created. + * + * @return the message's Session + * @since JavaMail 1.5 + */ + public Session getSession() { + return session; + } + + /** + * Returns the "From" attribute. The "From" attribute contains + * the identity of the person(s) who wished this message to + * be sent.

+ * + * In certain implementations, this may be different + * from the entity that actually sent the message.

+ * + * This method returns null if this attribute + * is not present in this message. Returns an empty array if + * this attribute is present, but contains no addresses. + * + * @return array of Address objects + * @exception MessagingException for failures + */ + public abstract Address[] getFrom() throws MessagingException; + + /** + * Set the "From" attribute in this Message. The value of this + * attribute is obtained from the property "mail.user". If this + * property is absent, the system property "user.name" is used. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + public abstract void setFrom() throws MessagingException; + + /** + * Set the "From" attribute in this Message. + * + * @param address the sender + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + public abstract void setFrom(Address address) + throws MessagingException; + + /** + * Add these addresses to the existing "From" attribute + * + * @param addresses the senders + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + public abstract void addFrom(Address[] addresses) + throws MessagingException; + + /** + * This inner class defines the types of recipients allowed by + * the Message class. The currently defined types are TO, + * CC and BCC. + * + * Note that this class only has a protected constructor, thereby + * restricting new Recipient types to either this class or subclasses. + * This effectively implements an enumeration of the allowed Recipient + * types. + * + * The following code sample shows how to use this class to obtain + * the "TO" recipients from a message. + *

+     *
+     * Message msg = folder.getMessages(1);
+     * Address[] a = m.getRecipients(Message.RecipientType.TO);
+     *
+     * 
+ * + * @see javax.mail.Message#getRecipients + * @see javax.mail.Message#setRecipients + * @see javax.mail.Message#addRecipients + */ + public static class RecipientType implements Serializable { + /** + * The "To" (primary) recipients. + */ + public static final RecipientType TO = new RecipientType("To"); + /** + * The "Cc" (carbon copy) recipients. + */ + public static final RecipientType CC = new RecipientType("Cc"); + /** + * The "Bcc" (blind carbon copy) recipients. + */ + public static final RecipientType BCC = new RecipientType("Bcc"); + + /** + * The type of recipient, usually the name of a corresponding + * Internet standard header. + * + * @serial + */ + protected String type; + + private static final long serialVersionUID = -7479791750606340008L; + + /** + * Constructor for use by subclasses. + * + * @param type the recipient type + */ + protected RecipientType(String type) { + this.type = type; + } + + /** + * When deserializing a RecipientType, we need to make sure to + * return only one of the known static final instances defined + * in this class. Subclasses must implement their own + * readResolve method that checks for their known + * instances before calling this super method. + * + * @return the RecipientType object instance + * @exception ObjectStreamException for object stream errors + */ + protected Object readResolve() throws ObjectStreamException { + if (type.equals("To")) + return TO; + else if (type.equals("Cc")) + return CC; + else if (type.equals("Bcc")) + return BCC; + else + throw new InvalidObjectException( + "Attempt to resolve unknown RecipientType: " + type); + } + + @Override + public String toString() { + return type; + } + } + + /** + * Get all the recipient addresses of the given type.

+ * + * This method returns null if no recipients of + * the given type are present in this message. It may return an + * empty array if the header is present, but contains no addresses. + * + * @param type the recipient type + * @return array of Address objects + * @exception MessagingException for failures + * @see Message.RecipientType#TO + * @see Message.RecipientType#CC + * @see Message.RecipientType#BCC + */ + public abstract Address[] getRecipients(RecipientType type) + throws MessagingException; + + /** + * Get all the recipient addresses for the message. + * The default implementation extracts the TO, CC, and BCC + * recipients using the getRecipients method.

+ * + * This method returns null if none of the recipient + * headers are present in this message. It may Return an empty array + * if any recipient header is present, but contains no addresses. + * + * @return array of Address objects + * @exception MessagingException for failures + * @see Message.RecipientType#TO + * @see Message.RecipientType#CC + * @see Message.RecipientType#BCC + * @see #getRecipients + */ + public Address[] getAllRecipients() throws MessagingException { + Address[] to = getRecipients(RecipientType.TO); + Address[] cc = getRecipients(RecipientType.CC); + Address[] bcc = getRecipients(RecipientType.BCC); + + if (cc == null && bcc == null) + return to; // a common case + + int numRecip = + (to != null ? to.length : 0) + + (cc != null ? cc.length : 0) + + (bcc != null ? bcc.length : 0); + Address[] addresses = new Address[numRecip]; + int pos = 0; + if (to != null) { + System.arraycopy(to, 0, addresses, pos, to.length); + pos += to.length; + } + if (cc != null) { + System.arraycopy(cc, 0, addresses, pos, cc.length); + pos += cc.length; + } + if (bcc != null) { + System.arraycopy(bcc, 0, addresses, pos, bcc.length); + // pos += bcc.length; + } + return addresses; + } + + /** + * Set the recipient addresses. All addresses of the specified + * type are replaced by the addresses parameter. + * + * @param type the recipient type + * @param addresses the addresses + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + public abstract void setRecipients(RecipientType type, Address[] addresses) + throws MessagingException; + + /** + * Set the recipient address. All addresses of the specified + * type are replaced by the address parameter.

+ * + * The default implementation uses the setRecipients method. + * + * @param type the recipient type + * @param address the address + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception MessagingException for other failures + */ + public void setRecipient(RecipientType type, Address address) + throws MessagingException { + if (address == null) + setRecipients(type, null); + else { + Address[] a = new Address[1]; + a[0] = address; + setRecipients(type, a); + } + } + + /** + * Add these recipient addresses to the existing ones of the given type. + * + * @param type the recipient type + * @param addresses the addresses + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + public abstract void addRecipients(RecipientType type, Address[] addresses) + throws MessagingException; + + /** + * Add this recipient address to the existing ones of the given type.

+ * + * The default implementation uses the addRecipients method. + * + * @param type the recipient type + * @param address the address + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception MessagingException for other failures + */ + public void addRecipient(RecipientType type, Address address) + throws MessagingException { + Address[] a = new Address[1]; + a[0] = address; + addRecipients(type, a); + } + + /** + * Get the addresses to which replies should be directed. + * This will usually be the sender of the message, but + * some messages may direct replies to a different address.

+ * + * The default implementation simply calls the getFrom + * method.

+ * + * This method returns null if the corresponding + * header is not present. Returns an empty array if the header + * is present, but contains no addresses. + * + * @return addresses to which replies should be directed + * @exception MessagingException for failures + * @see #getFrom + */ + public Address[] getReplyTo() throws MessagingException { + return getFrom(); + } + + /** + * Set the addresses to which replies should be directed. + * (Normally only a single address will be specified.) + * Not all message types allow this to be specified separately + * from the sender of the message.

+ * + * The default implementation provided here just throws the + * MethodNotSupportedException. + * + * @param addresses addresses to which replies should be directed + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MethodNotSupportedException if the underlying + * implementation does not support setting this + * attribute + * @exception MessagingException for other failures + */ + public void setReplyTo(Address[] addresses) throws MessagingException { + throw new MethodNotSupportedException("setReplyTo not supported"); + } + + /** + * Get the subject of this message. + * + * @return the subject + * @exception MessagingException for failures + */ + public abstract String getSubject() throws MessagingException; + + /** + * Set the subject of this message. + * + * @param subject the subject + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + public abstract void setSubject(String subject) + throws MessagingException; + + /** + * Get the date this message was sent. + * + * @return the date this message was sent + * @exception MessagingException for failures + */ + public abstract Date getSentDate() throws MessagingException; + + /** + * Set the sent date of this message. + * + * @param date the sent date of this message + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + public abstract void setSentDate(Date date) throws MessagingException; + + /** + * Get the date this message was received. + * + * @return the date this message was received + * @exception MessagingException for failures + */ + public abstract Date getReceivedDate() throws MessagingException; + + /** + * Returns a Flags object containing the flags for + * this message.

+ * + * Modifying any of the flags in this returned Flags object will + * not affect the flags of this message. Use setFlags() + * to do that.

+ * + * @return Flags object containing the flags for this message + * @see javax.mail.Flags + * @see #setFlags + * @exception MessagingException for failures + */ + public abstract Flags getFlags() throws MessagingException; + + /** + * Check whether the flag specified in the flag + * argument is set in this message.

+ * + * The default implementation uses getFlags. + * + * @param flag the flag + * @return value of the specified flag for this message + * @see javax.mail.Flags.Flag + * @see javax.mail.Flags.Flag#ANSWERED + * @see javax.mail.Flags.Flag#DELETED + * @see javax.mail.Flags.Flag#DRAFT + * @see javax.mail.Flags.Flag#FLAGGED + * @see javax.mail.Flags.Flag#RECENT + * @see javax.mail.Flags.Flag#SEEN + * @exception MessagingException for failures + */ + public boolean isSet(Flags.Flag flag) throws MessagingException { + return getFlags().contains(flag); + } + + /** + * Set the specified flags on this message to the specified value. + * Note that any flags in this message that are not specified in + * the given Flags object are unaffected.

+ * + * This will result in a MessageChangedEvent being + * delivered to any MessageChangedListener registered on this + * Message's containing folder. + * + * @param flag Flags object containing the flags to be set + * @param set the value to be set + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values. + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + * @see javax.mail.event.MessageChangedEvent + */ + public abstract void setFlags(Flags flag, boolean set) + throws MessagingException; + + /** + * Set the specified flag on this message to the specified value. + * + * This will result in a MessageChangedEvent being + * delivered to any MessageChangedListener registered on this + * Message's containing folder.

+ * + * The default implementation uses the setFlags method. + * + * @param flag Flags.Flag object containing the flag to be set + * @param set the value to be set + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values. + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + * @see javax.mail.event.MessageChangedEvent + */ + public void setFlag(Flags.Flag flag, boolean set) + throws MessagingException { + Flags f = new Flags(flag); + setFlags(f, set); + } + + /** + * Get the Message number for this Message. + * A Message object's message number is the relative + * position of this Message in its Folder. Note that the message + * number for a particular Message can change during a session + * if other messages in the Folder are deleted and expunged.

+ * + * Valid message numbers start at 1. Messages that do not belong + * to any folder (like newly composed or derived messages) have 0 + * as their message number. + * + * @return the message number + */ + public int getMessageNumber() { + return msgnum; + } + + /** + * Set the Message number for this Message. This method is + * invoked only by the implementation classes. + * + * @param msgnum the message number + */ + protected void setMessageNumber(int msgnum) { + this.msgnum = msgnum; + } + + /** + * Get the folder from which this message was obtained. If + * this is a new message or nested message, this method returns + * null. + * + * @return the containing folder + */ + public Folder getFolder() { + return folder; + } + + /** + * Checks whether this message is expunged. All other methods except + * getMessageNumber() are invalid on an expunged + * Message object.

+ * + * Messages that are expunged due to an explict expunge() + * request on the containing Folder are removed from the Folder + * immediately. Messages that are externally expunged by another source + * are marked "expunged" and return true for the isExpunged() method, + * but they are not removed from the Folder until an explicit + * expunge() is done on the Folder.

+ * + * See the description of expunge() for more details on + * expunge handling. + * + * @return true if the message is expunged + * @see Folder#expunge + */ + public boolean isExpunged() { + return expunged; + } + + /** + * Sets the expunged flag for this Message. This method is to + * be used only by the implementation classes. + * + * @param expunged the expunged flag + */ + protected void setExpunged(boolean expunged) { + this.expunged = expunged; + } + + /** + * Get a new Message suitable for a reply to this message. + * The new Message will have its attributes and headers + * set up appropriately. Note that this new message object + * will be empty, that is, it will not have a "content". + * These will have to be suitably filled in by the client.

+ * + * If replyToAll is set, the new Message will be addressed + * to all recipients of this message. Otherwise, the reply will be + * addressed to only the sender of this message (using the value + * of the getReplyTo method).

+ * + * The "Subject" field is filled in with the original subject + * prefixed with "Re:" (unless it already starts with "Re:").

+ * + * The reply message will use the same session as this message. + * + * @param replyToAll reply should be sent to all recipients + * of this message + * @return the reply Message + * @exception MessagingException for failures + */ + public abstract Message reply(boolean replyToAll) throws MessagingException; + + /** + * Save any changes made to this message into the message-store + * when the containing folder is closed, if the message is contained + * in a folder. (Some implementations may save the changes + * immediately.) Update any header fields to be consistent with the + * changed message contents. If any part of a message's headers or + * contents are changed, saveChanges must be called to ensure that + * those changes are permanent. If saveChanges is not called, any + * such modifications may or may not be saved, depending on the + * message store and folder implementation.

+ * + * Messages obtained from folders opened READ_ONLY should not be + * modified and saveChanges should not be called on such messages. + * + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values. + * @exception MessagingException for other failures + */ + public abstract void saveChanges() throws MessagingException; + + /** + * Apply the specified Search criterion to this message. + * + * @param term the Search criterion + * @return true if the Message matches this search + * criterion, false otherwise. + * @exception MessagingException for failures + * @see javax.mail.search.SearchTerm + */ + public boolean match(SearchTerm term) throws MessagingException { + return term.match(this); + } +} diff --git a/app/src/main/java/javax/mail/MessageAware.java b/app/src/main/java/javax/mail/MessageAware.java new file mode 100644 index 0000000000..7657fcc4bd --- /dev/null +++ b/app/src/main/java/javax/mail/MessageAware.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * An interface optionally implemented by DataSources to + * supply information to a DataContentHandler about the + * message context in which the data content object is operating. + * + * @see javax.mail.MessageContext + * @see javax.activation.DataSource + * @see javax.activation.DataContentHandler + * @since JavaMail 1.1 + */ +public interface MessageAware { + /** + * Return the message context. + * + * @return the message context + */ + public MessageContext getMessageContext(); +} diff --git a/app/src/main/java/javax/mail/MessageContext.java b/app/src/main/java/javax/mail/MessageContext.java new file mode 100644 index 0000000000..a1365cb8df --- /dev/null +++ b/app/src/main/java/javax/mail/MessageContext.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * The context in which a piece of Message content is contained. A + * MessageContext object is returned by the + * getMessageContext method of the + * MessageAware interface. MessageAware is + * typically implemented by DataSources to allow a + * DataContentHandler to pass on information about the + * context in which a data content object is operating. + * + * @see javax.mail.MessageAware + * @see javax.activation.DataSource + * @see javax.activation.DataContentHandler + * @since JavaMail 1.1 + */ +public class MessageContext { + private Part part; + + /** + * Create a MessageContext object describing the context of the given Part. + * + * @param part the Part + */ + public MessageContext(Part part) { + this.part = part; + } + + /** + * Return the Part that contains the content. + * + * @return the containing Part, or null if not known + */ + public Part getPart() { + return part; + } + + /** + * Return the Message that contains the content. + * Follows the parent chain up through containing Multipart + * objects until it comes to a Message object, or null. + * + * @return the containing Message, or null if not known + */ + public Message getMessage() { + try { + return getMessage(part); + } catch (MessagingException ex) { + return null; + } + } + + /** + * Return the Message containing an arbitrary Part. + * Follows the parent chain up through containing Multipart + * objects until it comes to a Message object, or null. + * + * @return the containing Message, or null if none + * @see javax.mail.BodyPart#getParent + * @see javax.mail.Multipart#getParent + */ + private static Message getMessage(Part p) throws MessagingException { + while (p != null) { + if (p instanceof Message) + return (Message)p; + BodyPart bp = (BodyPart)p; + Multipart mp = bp.getParent(); + if (mp == null) // MimeBodyPart might not be in a MimeMultipart + return null; + p = mp.getParent(); + } + return null; + } + + /** + * Return the Session we're operating in. + * + * @return the Session, or null if not known + */ + public Session getSession() { + Message msg = getMessage(); + return msg != null ? msg.getSession() : null; + } +} diff --git a/app/src/main/java/javax/mail/MessageRemovedException.java b/app/src/main/java/javax/mail/MessageRemovedException.java new file mode 100644 index 0000000000..28cde68753 --- /dev/null +++ b/app/src/main/java/javax/mail/MessageRemovedException.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * The exception thrown when an invalid method is invoked on an expunged + * Message. The only valid methods on an expunged Message are + * isExpunged() and getMessageNumber(). + * + * @see javax.mail.Message#isExpunged() + * @see javax.mail.Message#getMessageNumber() + * @author John Mani + */ + +public class MessageRemovedException extends MessagingException { + + private static final long serialVersionUID = 1951292550679528690L; + + /** + * Constructs a MessageRemovedException with no detail message. + */ + public MessageRemovedException() { + super(); + } + + /** + * Constructs a MessageRemovedException with the specified + * detail message. + * + * @param s The detailed error message + */ + public MessageRemovedException(String s) { + super(s); + } + + /** + * Constructs a MessageRemovedException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param s The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public MessageRemovedException(String s, Exception e) { + super(s, e); + } +} diff --git a/app/src/main/java/javax/mail/MessagingException.java b/app/src/main/java/javax/mail/MessagingException.java new file mode 100644 index 0000000000..803a4afc78 --- /dev/null +++ b/app/src/main/java/javax/mail/MessagingException.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.lang.*; + +/** + * The base class for all exceptions thrown by the Messaging classes + * + * @author John Mani + * @author Bill Shannon + */ + +public class MessagingException extends Exception { + + /** + * The next exception in the chain. + * + * @serial + */ + private Exception next; + + private static final long serialVersionUID = -7569192289819959253L; + + /** + * Constructs a MessagingException with no detail message. + */ + public MessagingException() { + super(); + initCause(null); // prevent anyone else from setting it + } + + /** + * Constructs a MessagingException with the specified detail message. + * + * @param s the detail message + */ + public MessagingException(String s) { + super(s); + initCause(null); // prevent anyone else from setting it + } + + /** + * Constructs a MessagingException with the specified + * Exception and detail message. The specified exception is chained + * to this exception. + * + * @param s the detail message + * @param e the embedded exception + * @see #getNextException + * @see #setNextException + * @see #getCause + */ + public MessagingException(String s, Exception e) { + super(s); + next = e; + initCause(null); // prevent anyone else from setting it + } + + /** + * Get the next exception chained to this one. If the + * next exception is a MessagingException, the chain + * may extend further. + * + * @return next Exception, null if none. + */ + public synchronized Exception getNextException() { + return next; + } + + /** + * Overrides the getCause method of Throwable + * to return the next exception in the chain of nested exceptions. + * + * @return next Exception, null if none. + */ + @Override + public synchronized Throwable getCause() { + return next; + } + + /** + * Add an exception to the end of the chain. If the end + * is not a MessagingException, this + * exception cannot be added to the end. + * + * @param ex the new end of the Exception chain + * @return true if this Exception + * was added, false otherwise. + */ + public synchronized boolean setNextException(Exception ex) { + Exception theEnd = this; + while (theEnd instanceof MessagingException && + ((MessagingException)theEnd).next != null) { + theEnd = ((MessagingException)theEnd).next; + } + // If the end is a MessagingException, we can add this + // exception to the chain. + if (theEnd instanceof MessagingException) { + ((MessagingException)theEnd).next = ex; + return true; + } else + return false; + } + + /** + * Override toString method to provide information on + * nested exceptions. + */ + @Override + public synchronized String toString() { + String s = super.toString(); + Exception n = next; + if (n == null) + return s; + StringBuilder sb = new StringBuilder(s == null ? "" : s); + while (n != null) { + sb.append(";\n nested exception is:\n\t"); + if (n instanceof MessagingException) { + MessagingException mex = (MessagingException)n; + sb.append(mex.superToString()); + n = mex.next; + } else { + sb.append(n.toString()); + n = null; + } + } + return sb.toString(); + } + + /** + * Return the "toString" information for this exception, + * without any information on nested exceptions. + */ + private final String superToString() { + return super.toString(); + } +} diff --git a/app/src/main/java/javax/mail/MethodNotSupportedException.java b/app/src/main/java/javax/mail/MethodNotSupportedException.java new file mode 100644 index 0000000000..bf7289fcc9 --- /dev/null +++ b/app/src/main/java/javax/mail/MethodNotSupportedException.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + + +/** + * The exception thrown when a method is not supported by the + * implementation + * + * @author John Mani + */ + +public class MethodNotSupportedException extends MessagingException { + + private static final long serialVersionUID = -3757386618726131322L; + + /** + * Constructs a MethodNotSupportedException with no detail message. + */ + public MethodNotSupportedException() { + super(); + } + + /** + * Constructs a MethodNotSupportedException with the specified + * detail message. + * + * @param s The detailed error message + */ + public MethodNotSupportedException(String s) { + super(s); + } + + /** + * Constructs a MethodNotSupportedException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param s The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public MethodNotSupportedException(String s, Exception e) { + super(s, e); + } +} diff --git a/app/src/main/java/javax/mail/Multipart.java b/app/src/main/java/javax/mail/Multipart.java new file mode 100644 index 0000000000..d1bcc27b05 --- /dev/null +++ b/app/src/main/java/javax/mail/Multipart.java @@ -0,0 +1,260 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.util.Vector; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; +import javax.activation.DataSource; + +/** + * Multipart is a container that holds multiple body parts. Multipart + * provides methods to retrieve and set its subparts.

+ * + * Multipart also acts as the base class for the content object returned + * by most Multipart DataContentHandlers. For example, invoking getContent() + * on a DataHandler whose source is a "multipart/signed" data source may + * return an appropriate subclass of Multipart.

+ * + * Some messaging systems provide different subtypes of Multiparts. For + * example, MIME specifies a set of subtypes that include "alternative", + * "mixed", "related", "parallel", "signed", etc.

+ * + * Multipart is an abstract class. Subclasses provide actual implementations. + * + * @author John Mani + */ + +public abstract class Multipart { + + /** + * Vector of BodyPart objects. + */ + protected Vector parts = new Vector<>(); // Holds BodyParts + + /** + * This field specifies the content-type of this multipart + * object. It defaults to "multipart/mixed". + */ + protected String contentType = "multipart/mixed"; // Content-Type + + /** + * The Part containing this Multipart, + * if known. + * @since JavaMail 1.1 + */ + protected Part parent; + + /** + * Default constructor. An empty Multipart object is created. + */ + protected Multipart() { } + + /** + * Setup this Multipart object from the given MultipartDataSource.

+ * + * The method adds the MultipartDataSource's BodyPart + * objects into this Multipart. This Multipart's contentType is + * set to that of the MultipartDataSource.

+ * + * This method is typically used in those cases where one + * has a multipart data source that has already been pre-parsed into + * the individual body parts (for example, an IMAP datasource), but + * needs to create an appropriate Multipart subclass that represents + * a specific multipart subtype. + * + * @param mp Multipart datasource + * @exception MessagingException for failures + */ + protected synchronized void setMultipartDataSource(MultipartDataSource mp) + throws MessagingException { + contentType = mp.getContentType(); + + int count = mp.getCount(); + for (int i = 0; i < count; i++) + addBodyPart(mp.getBodyPart(i)); + } + + /** + * Return the content-type of this Multipart.

+ * + * This implementation just returns the value of the + * contentType field. + * + * @return content-type + * @see #contentType + */ + public synchronized String getContentType() { + return contentType; + } + + /** + * Return the number of enclosed BodyPart objects.

+ * + * @return number of parts + * @exception MessagingException for failures + * @see #parts + */ + public synchronized int getCount() throws MessagingException { + if (parts == null) + return 0; + + return parts.size(); + } + + /** + * Get the specified Part. Parts are numbered starting at 0. + * + * @param index the index of the desired Part + * @return the Part + * @exception IndexOutOfBoundsException if the given index + * is out of range. + * @exception MessagingException for other failures + */ + public synchronized BodyPart getBodyPart(int index) + throws MessagingException { + if (parts == null) + throw new IndexOutOfBoundsException("No such BodyPart"); + + return parts.elementAt(index); + } + + /** + * Remove the specified part from the multipart message. + * Shifts all the parts after the removed part down one. + * + * @param part The part to remove + * @return true if part removed, false otherwise + * @exception MessagingException if no such Part exists + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + */ + public synchronized boolean removeBodyPart(BodyPart part) + throws MessagingException { + if (parts == null) + throw new MessagingException("No such body part"); + + boolean ret = parts.removeElement(part); + part.setParent(null); + return ret; + } + + /** + * Remove the part at specified location (starting from 0). + * Shifts all the parts after the removed part down one. + * + * @param index Index of the part to remove + * @exception IndexOutOfBoundsException if the given index + * is out of range. + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception MessagingException for other failures + */ + public synchronized void removeBodyPart(int index) + throws MessagingException { + if (parts == null) + throw new IndexOutOfBoundsException("No such BodyPart"); + + BodyPart part = parts.elementAt(index); + parts.removeElementAt(index); + part.setParent(null); + } + + /** + * Adds a Part to the multipart. The BodyPart is appended to + * the list of existing Parts. + * + * @param part The Part to be appended + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception MessagingException for other failures + */ + public synchronized void addBodyPart(BodyPart part) + throws MessagingException { + if (parts == null) + parts = new Vector<>(); + + parts.addElement(part); + part.setParent(this); + } + + /** + * Adds a BodyPart at position index. + * If index is not the last one in the list, + * the subsequent parts are shifted up. If index + * is larger than the number of parts present, the + * BodyPart is appended to the end. + * + * @param part The BodyPart to be inserted + * @param index Location where to insert the part + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception MessagingException for other failures + */ + public synchronized void addBodyPart(BodyPart part, int index) + throws MessagingException { + if (parts == null) + parts = new Vector<>(); + + parts.insertElementAt(part, index); + part.setParent(this); + } + + /** + * Output an appropriately encoded bytestream to the given + * OutputStream. The implementation subclass decides the + * appropriate encoding algorithm to be used. The bytestream + * is typically used for sending. + * + * @param os the stream to write to + * @exception IOException if an IO related exception occurs + * @exception MessagingException for other failures + */ + public abstract void writeTo(OutputStream os) + throws IOException, MessagingException; + + /** + * Return the Part that contains this Multipart + * object, or null if not known. + * + * @return the parent Part + * @since JavaMail 1.1 + */ + public synchronized Part getParent() { + return parent; + } + + /** + * Set the parent of this Multipart to be the specified + * Part. Normally called by the Message + * or BodyPart setContent(Multipart) method. + * parent may be null if the + * Multipart is being removed from its containing + * Part. + * + * @param parent the parent Part + * @since JavaMail 1.1 + */ + public synchronized void setParent(Part parent) { + this.parent = parent; + } +} diff --git a/app/src/main/java/javax/mail/MultipartDataSource.java b/app/src/main/java/javax/mail/MultipartDataSource.java new file mode 100644 index 0000000000..0d68363c3e --- /dev/null +++ b/app/src/main/java/javax/mail/MultipartDataSource.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import javax.activation.DataSource; + +/** + * MultipartDataSource is a DataSource that contains body + * parts. This allows "mail aware" DataContentHandlers to + * be implemented more efficiently by being aware of such + * DataSources and using the appropriate methods to access + * BodyParts.

+ * + * Note that the data of a MultipartDataSource is also available as + * an input stream.

+ * + * This interface will typically be implemented by providers that + * preparse multipart bodies, for example an IMAP provider. + * + * @author John Mani + * @see javax.activation.DataSource + */ + +public interface MultipartDataSource extends DataSource { + + /** + * Return the number of enclosed BodyPart objects. + * + * @return number of parts + */ + public int getCount(); + + /** + * Get the specified Part. Parts are numbered starting at 0. + * + * @param index the index of the desired Part + * @return the Part + * @exception IndexOutOfBoundsException if the given index + * is out of range. + * @exception MessagingException for other failures + */ + public BodyPart getBodyPart(int index) throws MessagingException; + +} diff --git a/app/src/main/java/javax/mail/NoSuchProviderException.java b/app/src/main/java/javax/mail/NoSuchProviderException.java new file mode 100644 index 0000000000..f94527d7eb --- /dev/null +++ b/app/src/main/java/javax/mail/NoSuchProviderException.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * This exception is thrown when Session attempts to instantiate a + * Provider that doesn't exist. + * + * @author Max Spivak + */ + +public class NoSuchProviderException extends MessagingException { + + private static final long serialVersionUID = 8058319293154708827L; + + /** + * Constructs a NoSuchProviderException with no detail message. + */ + public NoSuchProviderException() { + super(); + } + + /** + * Constructs a NoSuchProviderException with the specified + * detail message. + * + * @param message The detailed error message + */ + public NoSuchProviderException(String message) { + super(message); + } + + /** + * Constructs a NoSuchProviderException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param message The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public NoSuchProviderException(String message, Exception e) { + super(message, e); + } +} diff --git a/app/src/main/java/javax/mail/Part.java b/app/src/main/java/javax/mail/Part.java new file mode 100644 index 0000000000..8403666c0e --- /dev/null +++ b/app/src/main/java/javax/mail/Part.java @@ -0,0 +1,451 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.io.*; +import java.util.Enumeration; +import javax.activation.DataHandler; + +/** + * The Part interface is the common base interface for + * Messages and BodyParts.

+ * + * Part consists of a set of attributes and a "Content".

+ * + * Attributes:

+ * + * The Jakarta Mail API defines a set of standard Part attributes that are + * considered to be common to most existing Mail systems. These + * attributes have their own settor and gettor methods. Mail systems + * may support other Part attributes as well, these are represented as + * name-value pairs where both the name and value are Strings.

+ * + * Content:

+ * + * The data type of the "content" is returned by + * the getContentType() method. The MIME typing system + * is used to name data types.

+ * + * The "content" of a Part is available in various formats: + *

    + *
  • As a DataHandler - using the getDataHandler() method. + * The "content" of a Part is also available through a + * javax.activation.DataHandler object. The DataHandler + * object allows clients to discover the operations available on the + * content, and to instantiate the appropriate component to perform + * those operations. + * + *
  • As an input stream - using the getInputStream() method. + * Any mail-specific encodings are decoded before this stream is returned. + * + *
  • As a Java object - using the getContent() method. + * This method returns the "content" as a Java object. + * The returned object is of course dependent on the content + * itself. In particular, a "multipart" Part's content is always a + * Multipart or subclass thereof. That is, getContent() on a + * "multipart" type Part will always return a Multipart (or subclass) object. + *
+ * + * Part provides the writeTo() method that streams + * out its bytestream in mail-safe form suitable for transmission. + * This bytestream is typically an aggregation of the Part attributes + * and its content's bytestream.

+ * + * Message and BodyPart implement the Part interface. Note that in + * MIME parlance, Part models an Entity (RFC 2045, Section 2.4). + * + * @author John Mani + */ + +public interface Part { + + /** + * Return the size of the content of this part in bytes. + * Return -1 if the size cannot be determined.

+ * + * Note that the size may not be an exact measure of the content + * size and may or may not account for any transfer encoding + * of the content. The size is appropriate for display in a + * user interface to give the user a rough idea of the size + * of this part. + * + * @return size of content in bytes + * @exception MessagingException for failures + */ + public int getSize() throws MessagingException; + + /** + * Return the number of lines in the content of this part. + * Return -1 if the number cannot be determined. + * + * Note that this number may not be an exact measure of the + * content length and may or may not account for any transfer + * encoding of the content. + * + * @return number of lines in the content. + * @exception MessagingException for failures + */ + public int getLineCount() throws MessagingException; + + /** + * Returns the Content-Type of the content of this part. + * Returns null if the Content-Type could not be determined.

+ * + * The MIME typing system is used to name Content-types. + * + * @return The ContentType of this part + * @exception MessagingException for failures + * @see javax.activation.DataHandler + */ + public String getContentType() throws MessagingException; + + /** + * Is this Part of the specified MIME type? This method + * compares only the primaryType and + * subType. + * The parameters of the content types are ignored.

+ * + * For example, this method will return true when + * comparing a Part of content type "text/plain" + * with "text/plain; charset=foobar".

+ * + * If the subType of mimeType is the + * special character '*', then the subtype is ignored during the + * comparison. + * + * @param mimeType the MIME type to test + * @return true if this part is of the specified type + * @exception MessagingException for failures + */ + public boolean isMimeType(String mimeType) throws MessagingException; + + /** + * This part should be presented as an attachment. + * @see #getDisposition + * @see #setDisposition + */ + public static final String ATTACHMENT = "attachment"; + + /** + * This part should be presented inline. + * @see #getDisposition + * @see #setDisposition + */ + public static final String INLINE = "inline"; + + /** + * Return the disposition of this part. The disposition + * describes how the part should be presented to the user. + * (See RFC 2183.) The return value should be considered + * without regard to case. For example: + *

+     * String disp = part.getDisposition();
+     * if (disp == null || disp.equalsIgnoreCase(Part.ATTACHMENT))
+     *	// treat as attachment if not first part
+     * 
+ * + * @return disposition of this part, or null if unknown + * @exception MessagingException for failures + * @see #ATTACHMENT + * @see #INLINE + * @see #getFileName + */ + public String getDisposition() throws MessagingException; + + /** + * Set the disposition of this part. + * + * @param disposition disposition of this part + * @exception IllegalWriteException if the underlying implementation + * does not support modification of this header + * @exception IllegalStateException if this Part is obtained + * from a READ_ONLY folder + * @exception MessagingException for other failures + * @see #ATTACHMENT + * @see #INLINE + * @see #setFileName + */ + public void setDisposition(String disposition) throws MessagingException; + + /** + * Return a description String for this part. This typically + * associates some descriptive information with this part. + * Returns null if none is available. + * + * @return description of this part + * @exception MessagingException for failures + */ + public String getDescription() throws MessagingException; + + /** + * Set a description String for this part. This typically + * associates some descriptive information with this part. + * + * @param description description of this part + * @exception IllegalWriteException if the underlying implementation + * does not support modification of this header + * @exception IllegalStateException if this Part is obtained + * from a READ_ONLY folder + * @exception MessagingException for other failures + */ + public void setDescription(String description) throws MessagingException; + + /** + * Get the filename associated with this part, if possible. + * Useful if this part represents an "attachment" that was + * loaded from a file. The filename will usually be a simple + * name, not including directory components. + * + * @return Filename to associate with this part + * @exception MessagingException for failures + */ + public String getFileName() throws MessagingException; + + /** + * Set the filename associated with this part, if possible. + * Useful if this part represents an "attachment" that was + * loaded from a file. The filename will usually be a simple + * name, not including directory components. + * + * @param filename Filename to associate with this part + * @exception IllegalWriteException if the underlying implementation + * does not support modification of this header + * @exception IllegalStateException if this Part is obtained + * from a READ_ONLY folder + * @exception MessagingException for other failures + */ + public void setFileName(String filename) throws MessagingException; + + /** + * Return an input stream for this part's "content". Any + * mail-specific transfer encodings will be decoded before the + * input stream is provided.

+ * + * This is typically a convenience method that just invokes + * the DataHandler's getInputStream() method. + * + * @return an InputStream + * @exception IOException this is typically thrown by the + * DataHandler. Refer to the documentation for + * javax.activation.DataHandler for more details. + * @exception MessagingException for other failures + * @see #getDataHandler + * @see javax.activation.DataHandler#getInputStream + */ + public InputStream getInputStream() + throws IOException, MessagingException; + + /** + * Return a DataHandler for the content within this part. The + * DataHandler allows clients to operate on as well as retrieve + * the content. + * + * @return DataHandler for the content + * @exception MessagingException for failures + */ + public DataHandler getDataHandler() throws MessagingException; + + /** + * Return the content as a Java object. The type of the returned + * object is of course dependent on the content itself. For example, + * the object returned for "text/plain" content is usually a String + * object. The object returned for a "multipart" content is always a + * Multipart subclass. For content-types that are unknown to the + * DataHandler system, an input stream is returned as the content

+ * + * This is a convenience method that just invokes the DataHandler's + * getContent() method + * + * @return Object + * @exception IOException this is typically thrown by the + * DataHandler. Refer to the documentation for + * javax.activation.DataHandler for more details. + * @exception MessagingException for other failures + * + * @see javax.activation.DataHandler#getContent + */ + public Object getContent() throws IOException, MessagingException; + + /** + * This method provides the mechanism to set this part's content. + * The DataHandler wraps around the actual content. + * + * @param dh The DataHandler for the content. + * @exception IllegalWriteException if the underlying implementation + * does not support modification of existing values + * @exception IllegalStateException if this Part is obtained + * from a READ_ONLY folder + * @exception MessagingException for other failures + */ + public void setDataHandler(DataHandler dh) throws MessagingException; + + /** + * A convenience method for setting this part's content. The part + * internally wraps the content in a DataHandler.

+ * + * Note that a DataContentHandler class for the specified type should + * be available to the Jakarta Mail implementation for this to work right. + * i.e., to do setContent(foobar, "application/x-foobar"), + * a DataContentHandler for "application/x-foobar" should be installed. + * Refer to the Java Activation Framework for more information. + * + * @param obj A java object. + * @param type MIME type of this object. + * @exception IllegalWriteException if the underlying implementation + * does not support modification of existing values + * @exception IllegalStateException if this Part is obtained + * from a READ_ONLY folder + * @exception MessagingException for other failures + */ + public void setContent(Object obj, String type) + throws MessagingException; + + /** + * A convenience method that sets the given String as this + * part's content with a MIME type of "text/plain". + * + * @param text The text that is the Message's content. + * @exception IllegalWriteException if the underlying + * implementation does not support modification of + * existing values + * @exception IllegalStateException if this Part is obtained + * from a READ_ONLY folder + * @exception MessagingException for other failures + */ + public void setText(String text) throws MessagingException; + + /** + * This method sets the given Multipart object as this message's + * content. + * + * @param mp The multipart object that is the Message's content + * @exception IllegalWriteException if the underlying + * implementation does not support modification of + * existing values + * @exception IllegalStateException if this Part is obtained + * from a READ_ONLY folder + * @exception MessagingException for other failures + */ + public void setContent(Multipart mp) throws MessagingException; + + /** + * Output a bytestream for this Part. This bytestream is + * typically an aggregration of the Part attributes and + * an appropriately encoded bytestream from its 'content'.

+ * + * Classes that implement the Part interface decide on + * the appropriate encoding algorithm to be used.

+ * + * The bytestream is typically used for sending. + * + * @param os the stream to write to + * @exception IOException if an error occurs writing to the + * stream or if an error is generated + * by the javax.activation layer. + * @exception MessagingException if an error occurs fetching the + * data to be written + * + * @see javax.activation.DataHandler#writeTo + */ + public void writeTo(OutputStream os) throws IOException, MessagingException; + + /** + * Get all the headers for this header name. Returns null + * if no headers for this header name are available. + * + * @param header_name the name of this header + * @return the value fields for all headers with + * this name + * @exception MessagingException for failures + */ + public String[] getHeader(String header_name) + throws MessagingException; + + /** + * Set the value for this header_name. Replaces all existing + * header values with this new value. + * + * @param header_name the name of this header + * @param header_value the value for this header + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this Part is + * obtained from a READ_ONLY folder + * @exception MessagingException for other failures + */ + public void setHeader(String header_name, String header_value) + throws MessagingException; + /** + * Add this value to the existing values for this header_name. + * + * @param header_name the name of this header + * @param header_value the value for this header + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this Part is + * obtained from a READ_ONLY folder + * @exception MessagingException for other failures + */ + public void addHeader(String header_name, String header_value) + throws MessagingException; + /** + * Remove all headers with this name. + * + * @param header_name the name of this header + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this Part is + * obtained from a READ_ONLY folder + * @exception MessagingException for other failures + */ + public void removeHeader(String header_name) + throws MessagingException; + + /** + * Return all the headers from this part as an Enumeration of + * Header objects. + * + * @return enumeration of Header objects + * @exception MessagingException for failures + */ + public Enumeration

getAllHeaders() throws MessagingException; + + /** + * Return matching headers from this part as an Enumeration of + * Header objects. + * + * @param header_names the headers to match + * @return enumeration of Header objects + * @exception MessagingException for failures + */ + public Enumeration
getMatchingHeaders(String[] header_names) + throws MessagingException; + + /** + * Return non-matching headers from this envelope as an Enumeration + * of Header objects. + * + * @param header_names the headers to not match + * @return enumeration of Header objects + * @exception MessagingException for failures + */ + public Enumeration
getNonMatchingHeaders(String[] header_names) + throws MessagingException; +} diff --git a/app/src/main/java/javax/mail/PasswordAuthentication.java b/app/src/main/java/javax/mail/PasswordAuthentication.java new file mode 100644 index 0000000000..8654ee06b9 --- /dev/null +++ b/app/src/main/java/javax/mail/PasswordAuthentication.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + + +/** + * The class PasswordAuthentication is a data holder that is used by + * Authenticator. It is simply a repository for a user name and a password. + * + * @see java.net.PasswordAuthentication + * @see javax.mail.Authenticator + * @see javax.mail.Authenticator#getPasswordAuthentication() + * + * @author Bill Foote + */ + +public final class PasswordAuthentication { + + private final String userName; + private final String password; + + /** + * Initialize a new PasswordAuthentication + * @param userName the user name + * @param password The user's password + */ + public PasswordAuthentication(String userName, String password) { + this.userName = userName; + this.password = password; + } + + /** + * @return the user name + */ + public String getUserName() { + return userName; + } + + /** + * @return the password + */ + public String getPassword() { + return password; + } +} diff --git a/app/src/main/java/javax/mail/Provider.java b/app/src/main/java/javax/mail/Provider.java new file mode 100644 index 0000000000..6889875ede --- /dev/null +++ b/app/src/main/java/javax/mail/Provider.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * The Provider is a class that describes a protocol + * implementation. The values typically come from the + * javamail.providers and javamail.default.providers + * resource files. An application may also create and + * register a Provider object to dynamically add support + * for a new provider. + * + * @author Max Spivak + * @author Bill Shannon + */ +public class Provider { + + /** + * This inner class defines the Provider type. + * Currently, STORE and TRANSPORT are the only two provider types + * supported. + */ + + public static class Type { + public static final Type STORE = new Type("STORE"); + public static final Type TRANSPORT = new Type("TRANSPORT"); + + private String type; + + private Type(String type) { + this.type = type; + } + + @Override + public String toString() { + return type; + } + } + + private Type type; + private String protocol, className, vendor, version; + + /** + * Create a new provider of the specified type for the specified + * protocol. The specified class implements the provider. + * + * @param type Type.STORE or Type.TRANSPORT + * @param protocol valid protocol for the type + * @param classname class name that implements this protocol + * @param vendor optional string identifying the vendor (may be null) + * @param version optional implementation version string (may be null) + * @since JavaMail 1.4 + */ + public Provider(Type type, String protocol, String classname, + String vendor, String version) { + this.type = type; + this.protocol = protocol; + this.className = classname; + this.vendor = vendor; + this.version = version; + } + + /** + * Returns the type of this Provider. + * + * @return the provider type + */ + public Type getType() { + return type; + } + + /** + * Returns the protocol supported by this Provider. + * + * @return the protocol + */ + public String getProtocol() { + return protocol; + } + + /** + * Returns the name of the class that implements the protocol. + * + * @return the class name + */ + public String getClassName() { + return className; + } + + /** + * Returns the name of the vendor associated with this implementation + * or null. + * + * @return the vendor + */ + public String getVendor() { + return vendor; + } + + /** + * Returns the version of this implementation or null if no version. + * + * @return the version + */ + public String getVersion() { + return version; + } + + /** Overrides Object.toString() */ + @Override + public String toString() { + String s = "javax.mail.Provider[" + type + "," + + protocol + "," + className; + + if (vendor != null) + s += "," + vendor; + + if (version != null) + s += "," + version; + + s += "]"; + return s; + } +} diff --git a/app/src/main/java/javax/mail/Quota.java b/app/src/main/java/javax/mail/Quota.java new file mode 100644 index 0000000000..13da8b9826 --- /dev/null +++ b/app/src/main/java/javax/mail/Quota.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * This class represents a set of quotas for a given quota root. + * Each quota root has a set of resources, represented by the + * Quota.Resource class. Each resource has a name + * (for example, "STORAGE"), a current usage, and a usage limit. + * See RFC 2087. + * + * @since JavaMail 1.4 + * @author Bill Shannon + */ + +public class Quota { + + /** + * An individual resource in a quota root. + * + * @since JavaMail 1.4 + */ + public static class Resource { + /** The name of the resource. */ + public String name; + /** The current usage of the resource. */ + public long usage; + /** The usage limit for the resource. */ + public long limit; + + /** + * Construct a Resource object with the given name, + * usage, and limit. + * + * @param name the resource name + * @param usage the current usage of the resource + * @param limit the usage limit for the resource + */ + public Resource(String name, long usage, long limit) { + this.name = name; + this.usage = usage; + this.limit = limit; + } + } + + /** + * The name of the quota root. + */ + public String quotaRoot; + + /** + * The set of resources associated with this quota root. + */ + public Quota.Resource[] resources; + + /** + * Create a Quota object for the named quotaroot with no associated + * resources. + * + * @param quotaRoot the name of the quota root + */ + public Quota(String quotaRoot) { + this.quotaRoot = quotaRoot; + } + + /** + * Set a resource limit for this quota root. + * + * @param name the name of the resource + * @param limit the resource limit + */ + public void setResourceLimit(String name, long limit) { + if (resources == null) { + resources = new Quota.Resource[1]; + resources[0] = new Quota.Resource(name, 0, limit); + return; + } + for (int i = 0; i < resources.length; i++) { + if (resources[i].name.equalsIgnoreCase(name)) { + resources[i].limit = limit; + return; + } + } + Quota.Resource[] ra = new Quota.Resource[resources.length + 1]; + System.arraycopy(resources, 0, ra, 0, resources.length); + ra[ra.length - 1] = new Quota.Resource(name, 0, limit); + resources = ra; + } +} diff --git a/app/src/main/java/javax/mail/QuotaAwareStore.java b/app/src/main/java/javax/mail/QuotaAwareStore.java new file mode 100644 index 0000000000..a6f566d364 --- /dev/null +++ b/app/src/main/java/javax/mail/QuotaAwareStore.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * An interface implemented by Stores that support quotas. + * The {@link #getQuota getQuota} and {@link #setQuota setQuota} methods + * support the quota model defined by the IMAP QUOTA extension. + * Refer to RFC 2087 + * for more information.

+ * + * @since JavaMail 1.4 + */ +public interface QuotaAwareStore { + /** + * Get the quotas for the named folder. + * Quotas are controlled on the basis of a quota root, not + * (necessarily) a folder. The relationship between folders + * and quota roots depends on the server. Some servers + * might implement a single quota root for all folders owned by + * a user. Other servers might implement a separate quota root + * for each folder. A single folder can even have multiple + * quota roots, perhaps controlling quotas for different + * resources. + * + * @param folder the name of the folder + * @return array of Quota objects + * @exception MessagingException if the server doesn't support the + * QUOTA extension + */ + Quota[] getQuota(String folder) throws MessagingException; + + /** + * Set the quotas for the quota root specified in the quota argument. + * Typically this will be one of the quota roots obtained from the + * getQuota method, but it need not be. + * + * @param quota the quota to set + * @exception MessagingException if the server doesn't support the + * QUOTA extension + */ + void setQuota(Quota quota) throws MessagingException; +} diff --git a/app/src/main/java/javax/mail/ReadOnlyFolderException.java b/app/src/main/java/javax/mail/ReadOnlyFolderException.java new file mode 100644 index 0000000000..f658b5ca33 --- /dev/null +++ b/app/src/main/java/javax/mail/ReadOnlyFolderException.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * This exception is thrown when an attempt is made to open a folder + * read-write access when the folder is marked read-only.

+ * + * The getMessage() method returns more detailed information about the + * error that caused this exception.

+ * + * @author Jim Glennon + */ + +public class ReadOnlyFolderException extends MessagingException { + transient private Folder folder; + + private static final long serialVersionUID = 5711829372799039325L; + + /** + * Constructs a ReadOnlyFolderException with the specified + * folder and no detail message. + * + * @param folder the Folder + * @since JavaMail 1.2 + */ + public ReadOnlyFolderException(Folder folder) { + this(folder, null); + } + + /** + * Constructs a ReadOnlyFolderException with the specified + * detail message. + * + * @param folder The Folder + * @param message The detailed error message + * @since JavaMail 1.2 + */ + public ReadOnlyFolderException(Folder folder, String message) { + super(message); + this.folder = folder; + } + + /** + * Constructs a ReadOnlyFolderException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param folder The Folder + * @param message The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public ReadOnlyFolderException(Folder folder, String message, Exception e) { + super(message, e); + this.folder = folder; + } + + /** + * Returns the Folder object. + * + * @return the Folder + * @since JavaMail 1.2 + */ + public Folder getFolder() { + return folder; + } +} diff --git a/app/src/main/java/javax/mail/SendFailedException.java b/app/src/main/java/javax/mail/SendFailedException.java new file mode 100644 index 0000000000..ed73480c58 --- /dev/null +++ b/app/src/main/java/javax/mail/SendFailedException.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * This exception is thrown when the message cannot be sent.

+ * + * The exception includes those addresses to which the message could not be + * sent as well as the valid addresses to which the message was sent and + * valid addresses to which the message was not sent. + * + * @see javax.mail.Transport#send + * @see javax.mail.Transport#sendMessage + * @see javax.mail.event.TransportEvent + * + * @author John Mani + * @author Max Spivak + */ + +public class SendFailedException extends MessagingException { + transient protected Address[] invalid; + transient protected Address[] validSent; + transient protected Address[] validUnsent; + + private static final long serialVersionUID = -6457531621682372913L; + + /** + * Constructs a SendFailedException with no detail message. + */ + public SendFailedException() { + super(); + } + + /** + * Constructs a SendFailedException with the specified detail message. + * @param s the detail message + */ + public SendFailedException(String s) { + super(s); + } + + /** + * Constructs a SendFailedException with the specified + * Exception and detail message. The specified exception is chained + * to this exception. + * @param s the detail message + * @param e the embedded exception + * @see #getNextException + * @see #setNextException + */ + public SendFailedException(String s, Exception e) { + super(s, e); + } + + + /** + * Constructs a SendFailedException with the specified string + * and the specified address objects. + * + * @param msg the detail message + * @param ex the embedded exception + * @param validSent valid addresses to which message was sent + * @param validUnsent valid addresses to which message was not sent + * @param invalid the invalid addresses + * @see #getNextException + * @see #setNextException + */ + public SendFailedException(String msg, Exception ex, Address[] validSent, + Address[] validUnsent, Address[] invalid) { + super(msg, ex); + this.validSent = validSent; + this.validUnsent = validUnsent; + this.invalid = invalid; + } + + /** + * Return the addresses to which this message was sent succesfully. + * @return Addresses to which the message was sent successfully or null + */ + public Address[] getValidSentAddresses() { + return validSent; + } + + /** + * Return the addresses that are valid but to which this message + * was not sent. + * @return Addresses that are valid but to which the message was + * not sent successfully or null + */ + public Address[] getValidUnsentAddresses() { + return validUnsent; + } + + /** + * Return the addresses to which this message could not be sent. + * + * @return Addresses to which the message sending failed or null; + */ + public Address[] getInvalidAddresses() { + return invalid; + } +} diff --git a/app/src/main/java/javax/mail/Service.java b/app/src/main/java/javax/mail/Service.java new file mode 100644 index 0000000000..5c9e20c0a1 --- /dev/null +++ b/app/src/main/java/javax/mail/Service.java @@ -0,0 +1,657 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.io.*; +import java.net.*; +import java.util.*; +import java.util.concurrent.Executor; +import javax.mail.event.*; + +/** + * An abstract class that contains the functionality + * common to messaging services, such as stores and transports.

+ * A messaging service is created from a Session and is + * named using a URLName. A service must be connected + * before it can be used. Connection events are sent to reflect + * its connection status. + * + * @author Christopher Cotton + * @author Bill Shannon + * @author Kanwar Oberoi + */ + +public abstract class Service implements AutoCloseable { + + /** + * The session from which this service was created. + */ + protected Session session; + + /** + * The URLName of this service. + */ + protected volatile URLName url = null; + + /** + * Debug flag for this service. Set from the session's debug + * flag when this service is created. + */ + protected boolean debug = false; + + private boolean connected = false; + + /* + * connectionListeners is a Vector, initialized here, + * because we depend on it always existing and depend + * on the synchronization that Vector provides. + * (Sychronizing on the Service object itself can cause + * deadlocks when notifying listeners.) + */ + private final Vector connectionListeners + = new Vector<>(); + + /** + * The queue of events to be delivered. + */ + private final EventQueue q; + + /** + * Constructor. + * + * @param session Session object for this service + * @param urlname URLName object to be used for this service + */ + protected Service(Session session, URLName urlname) { + this.session = session; + debug = session.getDebug(); + url = urlname; + + /* + * Initialize the URLName with default values. + * The URLName will be updated when connect is called. + */ + String protocol = null; + String host = null; + int port = -1; + String user = null; + String password = null; + String file = null; + + // get whatever information we can from the URL + // XXX - url should always be non-null here, Session + // passes it into the constructor + if (url != null) { + protocol = url.getProtocol(); + host = url.getHost(); + port = url.getPort(); + user = url.getUsername(); + password = url.getPassword(); + file = url.getFile(); + } + + // try to get protocol-specific default properties + if (protocol != null) { + if (host == null) + host = session.getProperty("mail." + protocol + ".host"); + if (user == null) + user = session.getProperty("mail." + protocol + ".user"); + } + + // try to get mail-wide default properties + if (host == null) + host = session.getProperty("mail.host"); + + if (user == null) + user = session.getProperty("mail.user"); + + // try using the system username + if (user == null) { + try { + user = System.getProperty("user.name"); + } catch (SecurityException sex) { + // XXX - it's not worth creating a MailLogger just for this + //logger.log(Level.CONFIG, "Can't get user.name property", sex); + } + } + + url = new URLName(protocol, host, port, file, user, password); + + // create or choose the appropriate event queue + String scope = + session.getProperties().getProperty("mail.event.scope", "folder"); + Executor executor = + (Executor)session.getProperties().get("mail.event.executor"); + if (scope.equalsIgnoreCase("application")) + q = EventQueue.getApplicationEventQueue(executor); + else if (scope.equalsIgnoreCase("session")) + q = session.getEventQueue(); + else // if (scope.equalsIgnoreCase("store") || + // scope.equalsIgnoreCase("folder")) + q = new EventQueue(executor); + } + + /** + * A generic connect method that takes no parameters. Subclasses + * can implement the appropriate authentication schemes. Subclasses + * that need additional information might want to use some properties + * or might get it interactively using a popup window.

+ * + * If the connection is successful, an "open" ConnectionEvent + * is delivered to any ConnectionListeners on this service.

+ * + * Most clients should just call this method to connect to the service.

+ * + * It is an error to connect to an already connected service.

+ * + * The implementation provided here simply calls the following + * connect(String, String, String) method with nulls. + * + * @exception AuthenticationFailedException for authentication failures + * @exception MessagingException for other failures + * @exception IllegalStateException if the service is already connected + * + * @see javax.mail.event.ConnectionEvent + */ + public void connect() throws MessagingException { + connect(null, null, null); + } + + /** + * Connect to the specified address. This method provides a simple + * authentication scheme that requires a username and password.

+ * + * If the connection is successful, an "open" ConnectionEvent + * is delivered to any ConnectionListeners on this service.

+ * + * It is an error to connect to an already connected service.

+ * + * The implementation in the Service class will collect defaults + * for the host, user, and password from the session, from the + * URLName for this service, and from the supplied + * parameters and then call the protocolConnect method. + * If the protocolConnect method returns false, + * the user will be prompted for any missing information and the + * protocolConnect method will be called again. The + * subclass should override the protocolConnect method. + * The subclass should also implement the getURLName + * method, or use the implementation in this class.

+ * + * On a successful connection, the setURLName method is + * called with a URLName that includes the information used to make + * the connection, including the password.

+ * + * If the username passed in is null, a default value will be chosen + * as described above. + * + * If the password passed in is null and this is the first successful + * connection to this service, the user name and the password + * collected from the user will be saved as defaults for subsequent + * connection attempts to this same service when using other Service object + * instances (the connection information is typically always saved within + * a particular Service object instance). The password is saved using the + * Session method setPasswordAuthentication. If the + * password passed in is not null, it is not saved, on the assumption + * that the application is managing passwords explicitly. + * + * @param host the host to connect to + * @param user the user name + * @param password this user's password + * @exception AuthenticationFailedException for authentication failures + * @exception MessagingException for other failures + * @exception IllegalStateException if the service is already connected + * @see javax.mail.event.ConnectionEvent + * @see javax.mail.Session#setPasswordAuthentication + */ + public void connect(String host, String user, String password) + throws MessagingException { + connect(host, -1, user, password); + } + + /** + * Connect to the current host using the specified username + * and password. This method is equivalent to calling the + * connect(host, user, password) method with null + * for the host name. + * + * @param user the user name + * @param password this user's password + * @exception AuthenticationFailedException for authentication failures + * @exception MessagingException for other failures + * @exception IllegalStateException if the service is already connected + * @see javax.mail.event.ConnectionEvent + * @see javax.mail.Session#setPasswordAuthentication + * @see #connect(java.lang.String, java.lang.String, java.lang.String) + * @since JavaMail 1.4 + */ + public void connect(String user, String password) + throws MessagingException { + connect(null, user, password); + } + + /** + * Similar to connect(host, user, password) except a specific port + * can be specified. + * + * @param host the host to connect to + * @param port the port to connect to (-1 means the default port) + * @param user the user name + * @param password this user's password + * @exception AuthenticationFailedException for authentication failures + * @exception MessagingException for other failures + * @exception IllegalStateException if the service is already connected + * @see #connect(java.lang.String, java.lang.String, java.lang.String) + * @see javax.mail.event.ConnectionEvent + */ + public synchronized void connect(String host, int port, + String user, String password) throws MessagingException { + + // see if the service is already connected + if (isConnected()) + throw new IllegalStateException("already connected"); + + PasswordAuthentication pw; + boolean connected = false; + boolean save = false; + String protocol = null; + String file = null; + + // get whatever information we can from the URL + // XXX - url should always be non-null here, Session + // passes it into the constructor + if (url != null) { + protocol = url.getProtocol(); + if (host == null) + host = url.getHost(); + if (port == -1) + port = url.getPort(); + + if (user == null) { + user = url.getUsername(); + if (password == null) // get password too if we need it + password = url.getPassword(); + } else { + if (password == null && user.equals(url.getUsername())) + // only get the password if it matches the username + password = url.getPassword(); + } + + file = url.getFile(); + } + + // try to get protocol-specific default properties + if (protocol != null) { + if (host == null) + host = session.getProperty("mail." + protocol + ".host"); + if (user == null) + user = session.getProperty("mail." + protocol + ".user"); + } + + // try to get mail-wide default properties + if (host == null) + host = session.getProperty("mail.host"); + + if (user == null) + user = session.getProperty("mail.user"); + + // try using the system username + if (user == null) { + try { + user = System.getProperty("user.name"); + } catch (SecurityException sex) { + // XXX - it's not worth creating a MailLogger just for this + //logger.log(Level.CONFIG, "Can't get user.name property", sex); + } + } + + // if we don't have a password, look for saved authentication info + if (password == null && url != null) { + // canonicalize the URLName + setURLName(new URLName(protocol, host, port, file, user, null)); + pw = session.getPasswordAuthentication(getURLName()); + if (pw != null) { + if (user == null) { + user = pw.getUserName(); + password = pw.getPassword(); + } else if (user.equals(pw.getUserName())) { + password = pw.getPassword(); + } + } else + save = true; + } + + // try connecting, if the protocol needs some missing + // information (user, password) it will not connect. + // if it tries to connect and fails, remember why for later. + AuthenticationFailedException authEx = null; + try { + connected = protocolConnect(host, port, user, password); + } catch (AuthenticationFailedException ex) { + authEx = ex; + } + + // if not connected, ask the user and try again + if (!connected) { + InetAddress addr; + try { + addr = InetAddress.getByName(host); + } catch (UnknownHostException e) { + addr = null; + } + pw = session.requestPasswordAuthentication( + addr, port, + protocol, + null, user); + if (pw != null) { + user = pw.getUserName(); + password = pw.getPassword(); + + // have the service connect again + connected = protocolConnect(host, port, user, password); + } + } + + // if we're not connected by now, we give up + if (!connected) { + if (authEx != null) + throw authEx; + else if (user == null) + throw new AuthenticationFailedException( + "failed to connect, no user name specified?"); + else if (password == null) + throw new AuthenticationFailedException( + "failed to connect, no password specified?"); + else + throw new AuthenticationFailedException("failed to connect"); + } + + setURLName(new URLName(protocol, host, port, file, user, password)); + + if (save) + session.setPasswordAuthentication(getURLName(), + new PasswordAuthentication(user, password)); + + // set our connected state + setConnected(true); + + // finally, deliver the connection event + notifyConnectionListeners(ConnectionEvent.OPENED); + } + + + /** + * The service implementation should override this method to + * perform the actual protocol-specific connection attempt. + * The default implementation of the connect method + * calls this method as needed.

+ * + * The protocolConnect method should return + * false if a user name or password is required + * for authentication but the corresponding parameter is null; + * the connect method will prompt the user when + * needed to supply missing information. This method may + * also return false if authentication fails for + * the supplied user name or password. Alternatively, this method + * may throw an AuthenticationFailedException when authentication + * fails. This exception may include a String message with more + * detail about the failure.

+ * + * The protocolConnect method should throw an + * exception to report failures not related to authentication, + * such as an invalid host name or port number, loss of a + * connection during the authentication process, unavailability + * of the server, etc. + * + * @param host the name of the host to connect to + * @param port the port to use (-1 means use default port) + * @param user the name of the user to login as + * @param password the user's password + * @return true if connection successful, false if authentication failed + * @exception AuthenticationFailedException for authentication failures + * @exception MessagingException for non-authentication failures + */ + protected boolean protocolConnect(String host, int port, String user, + String password) throws MessagingException { + return false; + } + + /** + * Is this service currently connected?

+ * + * This implementation uses a private boolean field to + * store the connection state. This method returns the value + * of that field.

+ * + * Subclasses may want to override this method to verify that any + * connection to the message store is still alive. + * + * @return true if the service is connected, false if it is not connected + */ + public synchronized boolean isConnected() { + return connected; + } + + /** + * Set the connection state of this service. The connection state + * will automatically be set by the service implementation during the + * connect and close methods. + * Subclasses will need to call this method to set the state + * if the service was automatically disconnected.

+ * + * The implementation in this class merely sets the private field + * returned by the isConnected method. + * + * @param connected true if the service is connected, + * false if it is not connected + */ + protected synchronized void setConnected(boolean connected) { + this.connected = connected; + } + + /** + * Close this service and terminate its connection. A close + * ConnectionEvent is delivered to any ConnectionListeners. Any + * Messaging components (Folders, Messages, etc.) belonging to this + * service are invalid after this service is closed. Note that the service + * is closed even if this method terminates abnormally by throwing + * a MessagingException.

+ * + * This implementation uses setConnected(false) to set + * this service's connected state to false. It will then + * send a close ConnectionEvent to any registered ConnectionListeners. + * Subclasses overriding this method to do implementation specific + * cleanup should call this method as a last step to insure event + * notification, probably by including a call to super.close() + * in a finally clause. + * + * @see javax.mail.event.ConnectionEvent + * @throws MessagingException for errors while closing + */ + public synchronized void close() throws MessagingException { + setConnected(false); + notifyConnectionListeners(ConnectionEvent.CLOSED); + } + + /** + * Return a URLName representing this service. The returned URLName + * does not include the password field.

+ * + * Subclasses should only override this method if their + * URLName does not follow the standard format.

+ * + * The implementation in the Service class returns (usually a copy of) + * the url field with the password and file information + * stripped out. + * + * @return the URLName representing this service + * @see URLName + */ + public URLName getURLName() { + URLName url = this.url; // snapshot + if (url != null && (url.getPassword() != null || url.getFile() != null)) + return new URLName(url.getProtocol(), url.getHost(), + url.getPort(), null /* no file */, + url.getUsername(), null /* no password */); + else + return url; + } + + /** + * Set the URLName representing this service. + * Normally used to update the url field + * after a service has successfully connected.

+ * + * Subclasses should only override this method if their + * URL does not follow the standard format. In particular, + * subclasses should override this method if their URL + * does not require all the possible fields supported by + * URLName; a new URLName should + * be constructed with any unneeded fields removed.

+ * + * The implementation in the Service class simply sets the + * url field. + * + * @param url the URLName + * @see URLName + */ + protected void setURLName(URLName url) { + this.url = url; + } + + /** + * Add a listener for Connection events on this service.

+ * + * The default implementation provided here adds this listener + * to an internal list of ConnectionListeners. + * + * @param l the Listener for Connection events + * @see javax.mail.event.ConnectionEvent + */ + public void addConnectionListener(ConnectionListener l) { + connectionListeners.addElement(l); + } + + /** + * Remove a Connection event listener.

+ * + * The default implementation provided here removes this listener + * from the internal list of ConnectionListeners. + * + * @param l the listener + * @see #addConnectionListener + */ + public void removeConnectionListener(ConnectionListener l) { + connectionListeners.removeElement(l); + } + + /** + * Notify all ConnectionListeners. Service implementations are + * expected to use this method to broadcast connection events.

+ * + * The provided default implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * ConnectionListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param type the ConnectionEvent type + */ + protected void notifyConnectionListeners(int type) { + /* + * Don't bother queuing an event if there's no listeners. + * Yes, listeners could be removed after checking, which + * just makes this an expensive no-op. + */ + if (connectionListeners.size() > 0) { + ConnectionEvent e = new ConnectionEvent(this, type); + queueEvent(e, connectionListeners); + } + + /* Fix for broken JDK1.1.x Garbage collector : + * The 'conservative' GC in JDK1.1.x occasionally fails to + * garbage-collect Threads which are in the wait state. + * This would result in thread (and consequently memory) leaks. + * + * We attempt to fix this by sending a 'terminator' event + * to the queue, after we've sent the CLOSED event. The + * terminator event causes the event-dispatching thread to + * self destruct. + */ + if (type == ConnectionEvent.CLOSED) + q.terminateQueue(); + } + + /** + * Return getURLName.toString() if this service has a URLName, + * otherwise it will return the default toString. + */ + @Override + public String toString() { + URLName url = getURLName(); + if (url != null) + return url.toString(); + else + return super.toString(); + } + + /** + * Add the event and vector of listeners to the queue to be delivered. + * + * @param event the event + * @param vector the vector of listeners + */ + protected void queueEvent(MailEvent event, + Vector vector) { + /* + * Copy the vector in order to freeze the state of the set + * of EventListeners the event should be delivered to prior + * to delivery. This ensures that any changes made to the + * Vector from a target listener's method during the delivery + * of this event will not take effect until after the event is + * delivered. + */ + @SuppressWarnings("unchecked") + Vector v = (Vector)vector.clone(); + q.enqueue(event, v); + } + + /** + * Stop the event dispatcher thread so the queue can be garbage collected. + */ + @Override + protected void finalize() throws Throwable { + try { + q.terminateQueue(); + } finally { + super.finalize(); + } + } + + /** + * Package private method to allow Folder to get the Session for a Store. + */ + Session getSession() { + return session; + } + + /** + * Package private method to allow Folder to get the EventQueue for a Store. + */ + EventQueue getEventQueue() { + return q; + } +} diff --git a/app/src/main/java/javax/mail/Session.java b/app/src/main/java/javax/mail/Session.java new file mode 100644 index 0000000000..8bd9dd26e5 --- /dev/null +++ b/app/src/main/java/javax/mail/Session.java @@ -0,0 +1,1361 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.lang.reflect.*; +import java.io.*; +import java.net.*; +import java.security.*; +import java.util.Collections; +import java.util.Hashtable; +import java.util.Map; +import java.util.HashMap; +import java.util.List; +import java.util.ArrayList; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.ServiceLoader; +import java.util.logging.Level; +import java.util.concurrent.Executor; + +import com.sun.mail.util.LineInputStream; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.DefaultProvider; + +/** + * The Session class represents a mail session and is not subclassed. + * It collects together properties and defaults used by the mail API's. + * A single default session can be shared by multiple applications on the + * desktop. Unshared sessions can also be created.

+ * + * The Session class provides access to the protocol providers that + * implement the Store, Transport, and related + * classes. The protocol providers are configured using the following files: + *

    + *
  • javamail.providers and + * javamail.default.providers
  • + *
  • javamail.address.map and + * javamail.default.address.map
  • + *
+ *

+ * Each javamail.X resource file is searched for using + * three methods in the following order: + *

    + *
  1. java.home/conf/javamail.X
  2. + *
  3. META-INF/javamail.X
  4. + *
  5. META-INF/javamail.default.X
  6. + *
+ *

+ * (Where java.home is the value of the "java.home" System property + * and conf is the directory named "conf" if it exists, + * otherwise the directory named "lib"; the "conf" directory was + * introduced in JDK 1.9.) + *

+ * The first method allows the user to include their own version of the + * resource file by placing it in the conf directory where the + * java.home property points. The second method allows an + * application that uses the Jakarta Mail APIs to include their own resource + * files in their application's or jar file's META-INF + * directory. The javamail.default.X default files + * are part of the Jakarta Mail mail.jar file and should not be + * supplied by users.

+ * + * File location depends upon how the ClassLoader method + * getResource is implemented. Usually, the + * getResource method searches through CLASSPATH until it + * finds the requested file and then stops.

+ * + * The ordering of entries in the resource files matters. If multiple + * entries exist, the first entries take precedence over the later + * entries. For example, the first IMAP provider found will be set as the + * default IMAP implementation until explicitly changed by the + * application. The user- or system-supplied resource files augment, they + * do not override, the default files included with the Jakarta Mail APIs. + * This means that all entries in all files loaded will be available.

+ * + * javamail.providers and + * javamail.default.providers

+ * + * These resource files specify the stores and transports that are + * available on the system, allowing an application to "discover" what + * store and transport implementations are available. The protocol + * implementations are listed one per line. The file format defines four + * attributes that describe a protocol implementation. Each attribute is + * an "="-separated name-value pair with the name in lowercase. Each + * name-value pair is semi-colon (";") separated. The following names + * are defined. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
+ * Attribute Names in Providers Files + *
NameDescription
protocolName assigned to protocol. + * For example, smtp for Transport.
typeValid entries are store and transport.
classClass name that implements this protocol.
vendorOptional string identifying the vendor.
versionOptional string identifying the version.

+ * + * Here's an example of META-INF/javamail.default.providers + * file contents: + *

+ * protocol=imap; type=store; class=com.sun.mail.imap.IMAPStore; vendor=Oracle;
+ * protocol=smtp; type=transport; class=com.sun.mail.smtp.SMTPTransport; vendor=Oracle;
+ * 

+ * + * The current implementation also supports configuring providers using + * the Java SE {@link java.util.ServiceLoader ServiceLoader} mechanism. + * When creating your own provider, create a {@link Provider} subclass, + * for example: + *

+ * package com.example;
+ *
+ * import javax.mail.Provider;
+ *
+ * public class MyProvider extends Provider {
+ *     public MyProvider() {
+ *         super(Provider.Type.STORE, "myprot", MyStore.class.getName(),
+ *             "Example", null);
+ *     }
+ * }
+ * 
+ * Then include a file named META-INF/services/javax.mail.Provider + * in your jar file that lists the name of your Provider class: + *
+ * com.example.MyProvider
+ * 
+ *

+ * + * javamail.address.map and + * javamail.default.address.map

+ * + * These resource files map transport address types to the transport + * protocol. The getType method of + * javax.mail.Address returns the address type. The + * javamail.address.map file maps the transport type to the + * protocol. The file format is a series of name-value pairs. Each key + * name should correspond to an address type that is currently installed + * on the system; there should also be an entry for each + * javax.mail.Address implementation that is present if it is + * to be used. For example, the + * javax.mail.internet.InternetAddress method + * getType returns "rfc822". Each referenced protocol should + * be installed on the system. For the case of news, below, + * the client should install a Transport provider supporting the nntp + * protocol.

+ * + * Here are the typical contents of a javamail.address.map file: + *

+ * rfc822=smtp
+ * news=nntp
+ * 
+ * + * @author John Mani + * @author Bill Shannon + * @author Max Spivak + */ + +public final class Session { + + private final Properties props; + private final Authenticator authenticator; + private final Hashtable authTable + = new Hashtable<>(); + private boolean debug = false; + private PrintStream out; // debug output stream + private MailLogger logger; + private final List providers = new ArrayList<>(); + private final Map providersByProtocol = new HashMap<>(); + private final Map providersByClassName = new HashMap<>(); + private final Properties addressMap = new Properties(); + // maps type to protocol + // the queue of events to be delivered, if mail.event.scope===session + private final EventQueue q; + + // The default session. + private static Session defaultSession = null; + + private static final String confDir; + + static { + String dir = null; + try { + dir = AccessController.doPrivileged( + new PrivilegedAction() { + @Override + public String run() { + String home = System.getProperty("java.home"); + String newdir = home + File.separator + "conf"; + File conf = new File(newdir); + if (conf.exists()) + return newdir + File.separator; + else + return home + File.separator + + "lib" + File.separator; + } + }); + } catch (Exception ex) { + // ignore any exceptions + } + confDir = dir; + } + + // Constructor is not public + private Session(Properties props, Authenticator authenticator) { + this.props = props; + this.authenticator = authenticator; + + if (Boolean.valueOf(props.getProperty("mail.debug")).booleanValue()) + debug = true; + + initLogger(); + logger.log(Level.CONFIG, "Jakarta Mail version {0}", Version.version); + + // get the Class associated with the Authenticator + Class cl; + if (authenticator != null) + cl = authenticator.getClass(); + else + cl = this.getClass(); + // load the resources + loadProviders(cl); + loadAddressMap(cl); + q = new EventQueue((Executor)props.get("mail.event.executor")); + } + + private final synchronized void initLogger() { + logger = new MailLogger(this.getClass(), "DEBUG", debug, getDebugOut()); + } + + /** + * Get a new Session object. + * + * @param props Properties object that hold relevant properties.
+ * It is expected that the client supplies values + * for the properties listed in Appendix A of the + * Jakarta Mail spec (particularly mail.store.protocol, + * mail.transport.protocol, mail.host, mail.user, + * and mail.from) as the defaults are unlikely to + * work in all cases. + * @param authenticator Authenticator object used to call back to + * the application when a user name and password is + * needed. + * @return a new Session object + * @see javax.mail.Authenticator + */ + public static Session getInstance(Properties props, + Authenticator authenticator) { + return new Session(props, authenticator); + } + + /** + * Get a new Session object. + * + * @param props Properties object that hold relevant properties.
+ * It is expected that the client supplies values + * for the properties listed in Appendix A of the + * Jakarta Mail spec (particularly mail.store.protocol, + * mail.transport.protocol, mail.host, mail.user, + * and mail.from) as the defaults are unlikely to + * work in all cases. + * @return a new Session object + * @since JavaMail 1.2 + */ + public static Session getInstance(Properties props) { + return new Session(props, null); + } + + /** + * Get the default Session object. If a default has not yet been + * setup, a new Session object is created and installed as the + * default.

+ * + * Since the default session is potentially available to all + * code executing in the same Java virtual machine, and the session + * can contain security sensitive information such as user names + * and passwords, access to the default session is restricted. + * The Authenticator object, which must be created by the caller, + * is used indirectly to check access permission. The Authenticator + * object passed in when the session is created is compared with + * the Authenticator object passed in to subsequent requests to + * get the default session. If both objects are the same, or are + * from the same ClassLoader, the request is allowed. Otherwise, + * it is denied.

+ * + * Note that if the Authenticator object used to create the session + * is null, anyone can get the default session by passing in null.

+ * + * Note also that the Properties object is used only the first time + * this method is called, when a new Session object is created. + * Subsequent calls return the Session object that was created by the + * first call, and ignore the passed Properties object. Use the + * getInstance method to get a new Session object every + * time the method is called.

+ * + * Additional security Permission objects may be used to + * control access to the default session.

+ * + * In the current implementation, if a SecurityManager is set, the + * caller must have the RuntimePermission("setFactory") + * permission. + * + * @param props Properties object. Used only if a new Session + * object is created.
+ * It is expected that the client supplies values + * for the properties listed in Appendix A of the + * Jakarta Mail spec (particularly mail.store.protocol, + * mail.transport.protocol, mail.host, mail.user, + * and mail.from) as the defaults are unlikely to + * work in all cases. + * @param authenticator Authenticator object. Used only if a + * new Session object is created. Otherwise, + * it must match the Authenticator used to create + * the Session. + * @return the default Session object + */ + public static synchronized Session getDefaultInstance(Properties props, + Authenticator authenticator) { + if (defaultSession == null) { + SecurityManager security = System.getSecurityManager(); + if (security != null) + security.checkSetFactory(); + defaultSession = new Session(props, authenticator); + } else { + // have to check whether caller is allowed to see default session + if (defaultSession.authenticator == authenticator) + ; // either same object or both null, either way OK + else if (defaultSession.authenticator != null && + authenticator != null && + defaultSession.authenticator.getClass().getClassLoader() == + authenticator.getClass().getClassLoader()) + ; // both objects came from the same class loader, OK + else + // anything else is not allowed + throw new SecurityException("Access to default session denied"); + } + + return defaultSession; + } + + /** + * Get the default Session object. If a default has not yet been + * setup, a new Session object is created and installed as the + * default.

+ * + * Note that a default session created with no Authenticator is + * available to all code executing in the same Java virtual + * machine, and the session can contain security sensitive + * information such as user names and passwords. + * + * @param props Properties object. Used only if a new Session + * object is created.
+ * It is expected that the client supplies values + * for the properties listed in Appendix A of the + * Jakarta Mail spec (particularly mail.store.protocol, + * mail.transport.protocol, mail.host, mail.user, + * and mail.from) as the defaults are unlikely to + * work in all cases. + * @return the default Session object + * @since JavaMail 1.2 + */ + public static Session getDefaultInstance(Properties props) { + return getDefaultInstance(props, null); + } + + /** + * Set the debug setting for this Session. + *

+ * Since the debug setting can be turned on only after the Session + * has been created, to turn on debugging in the Session + * constructor, set the property mail.debug in the + * Properties object passed in to the constructor to true. The + * value of the mail.debug property is used to + * initialize the per-Session debugging flag. Subsequent calls to + * the setDebug method manipulate the per-Session + * debugging flag and have no affect on the mail.debug + * property. + * + * @param debug Debug setting + */ + public synchronized void setDebug(boolean debug) { + this.debug = debug; + initLogger(); + logger.log(Level.CONFIG, "setDebug: Jakarta Mail version {0}", + Version.version); + } + + /** + * Get the debug setting for this Session. + * + * @return current debug setting + */ + public synchronized boolean getDebug() { + return debug; + } + + /** + * Set the stream to be used for debugging output for this session. + * If out is null, System.out will be used. + * Note that debugging output that occurs before any session is created, + * as a result of setting the mail.debug system property, + * will always be sent to System.out. + * + * @param out the PrintStream to use for debugging output + * @since JavaMail 1.3 + */ + public synchronized void setDebugOut(PrintStream out) { + this.out = out; + initLogger(); + } + + /** + * Returns the stream to be used for debugging output. If no stream + * has been set, System.out is returned. + * + * @return the PrintStream to use for debugging output + * @since JavaMail 1.3 + */ + public synchronized PrintStream getDebugOut() { + if (out == null) + return System.out; + else + return out; + } + + /** + * This method returns an array of all the implementations installed + * via the javamail.[default.]providers files that can + * be loaded using the ClassLoader available to this application. + * + * @return Array of configured providers + */ + public synchronized Provider[] getProviders() { + Provider[] _providers = new Provider[providers.size()]; + providers.toArray(_providers); + return _providers; + } + + /** + * Returns the default Provider for the protocol + * specified. Checks mail.<protocol>.class property + * first and if it exists, returns the Provider + * associated with this implementation. If it doesn't exist, + * returns the Provider that appeared first in the + * configuration files. If an implementation for the protocol + * isn't found, throws NoSuchProviderException + * + * @param protocol Configured protocol (i.e. smtp, imap, etc) + * @return Currently configured Provider for the specified protocol + * @exception NoSuchProviderException If a provider for the given + * protocol is not found. + */ + public synchronized Provider getProvider(String protocol) + throws NoSuchProviderException { + + if (protocol == null || protocol.length() <= 0) { + throw new NoSuchProviderException("Invalid protocol: null"); + } + + Provider _provider = null; + + // check if the mail..class property exists + String _className = props.getProperty("mail."+protocol+".class"); + if (_className != null) { + if (logger.isLoggable(Level.FINE)) { + logger.fine("mail."+protocol+ + ".class property exists and points to " + + _className); + } + _provider = providersByClassName.get(_className); + } + + if (_provider != null) { + return _provider; + } else { + // returning currently default protocol in providersByProtocol + _provider = providersByProtocol.get(protocol); + } + + if (_provider == null) { + throw new NoSuchProviderException("No provider for " + protocol); + } else { + if (logger.isLoggable(Level.FINE)) { + logger.fine("getProvider() returning " + _provider.toString()); + } + return _provider; + } + } + + /** + * Set the passed Provider to be the default implementation + * for the protocol in Provider.protocol overriding any previous values. + * + * @param provider Currently configured Provider which will be + * set as the default for the protocol + * @exception NoSuchProviderException If the provider passed in + * is invalid. + */ + public synchronized void setProvider(Provider provider) + throws NoSuchProviderException { + if (provider == null) { + throw new NoSuchProviderException("Can't set null provider"); + } + providersByProtocol.put(provider.getProtocol(), provider); + providersByClassName.put(provider.getClassName(), provider); + props.put("mail." + provider.getProtocol() + ".class", + provider.getClassName()); + } + + + /** + * Get a Store object that implements this user's desired Store + * protocol. The mail.store.protocol property specifies the + * desired protocol. If an appropriate Store object is not obtained, + * NoSuchProviderException is thrown + * + * @return a Store object + * @exception NoSuchProviderException If a provider for the given + * protocol is not found. + */ + public Store getStore() throws NoSuchProviderException { + return getStore(getProperty("mail.store.protocol")); + } + + /** + * Get a Store object that implements the specified protocol. If an + * appropriate Store object cannot be obtained, + * NoSuchProviderException is thrown. + * + * @param protocol the Store protocol + * @return a Store object + * @exception NoSuchProviderException If a provider for the given + * protocol is not found. + */ + public Store getStore(String protocol) throws NoSuchProviderException { + return getStore(new URLName(protocol, null, -1, null, null, null)); + } + + + /** + * Get a Store object for the given URLName. If the requested Store + * object cannot be obtained, NoSuchProviderException is thrown. + * + * The "scheme" part of the URL string (Refer RFC 1738) is used + * to locate the Store protocol.

+ * + * @param url URLName that represents the desired Store + * @return a closed Store object + * @see #getFolder(URLName) + * @see javax.mail.URLName + * @exception NoSuchProviderException If a provider for the given + * URLName is not found. + */ + public Store getStore(URLName url) throws NoSuchProviderException { + String protocol = url.getProtocol(); + Provider p = getProvider(protocol); + return getStore(p, url); + } + + /** + * Get an instance of the store specified by Provider. Instantiates + * the store and returns it. + * + * @param provider Store Provider that will be instantiated + * @return Instantiated Store + * @exception NoSuchProviderException If a provider for the given + * Provider is not found. + */ + public Store getStore(Provider provider) throws NoSuchProviderException { + return getStore(provider, null); + } + + + /** + * Get an instance of the store specified by Provider. If the URLName + * is not null, uses it, otherwise creates a new one. Instantiates + * the store and returns it. This is a private method used by + * getStore(Provider) and getStore(URLName) + * + * @param provider Store Provider that will be instantiated + * @param url URLName used to instantiate the Store + * @return Instantiated Store + * @exception NoSuchProviderException If a provider for the given + * Provider/URLName is not found. + */ + private Store getStore(Provider provider, URLName url) + throws NoSuchProviderException { + + // make sure we have the correct type of provider + if (provider == null || provider.getType() != Provider.Type.STORE ) { + throw new NoSuchProviderException("invalid provider"); + } + + return getService(provider, url, Store.class); + } + + /** + * Get a closed Folder object for the given URLName. If the requested + * Folder object cannot be obtained, null is returned.

+ * + * The "scheme" part of the URL string (Refer RFC 1738) is used + * to locate the Store protocol. The rest of the URL string (that is, + * the "schemepart", as per RFC 1738) is used by that Store + * in a protocol dependent manner to locate and instantiate the + * appropriate Folder object.

+ * + * Note that RFC 1738 also specifies the syntax for the + * "schemepart" for IP-based protocols (IMAP4, POP3, etc.). + * Providers of IP-based mail Stores should implement that + * syntax for referring to Folders.

+ * + * @param url URLName that represents the desired folder + * @return Folder + * @see #getStore(URLName) + * @see javax.mail.URLName + * @exception NoSuchProviderException If a provider for the given + * URLName is not found. + * @exception MessagingException if the Folder could not be + * located or created. + */ + public Folder getFolder(URLName url) + throws MessagingException { + // First get the Store + Store store = getStore(url); + store.connect(); + return store.getFolder(url); + } + + /** + * Get a Transport object that implements this user's desired + * Transport protcol. The mail.transport.protocol property + * specifies the desired protocol. If an appropriate Transport + * object cannot be obtained, MessagingException is thrown. + * + * @return a Transport object + * @exception NoSuchProviderException If the provider is not found. + */ + public Transport getTransport() throws NoSuchProviderException { + String prot = getProperty("mail.transport.protocol"); + if (prot != null) + return getTransport(prot); + // if the property isn't set, use the protocol for "rfc822" + prot = (String)addressMap.get("rfc822"); + if (prot != null) + return getTransport(prot); + return getTransport("smtp"); // if all else fails + } + + /** + * Get a Transport object that implements the specified protocol. + * If an appropriate Transport object cannot be obtained, null is + * returned. + * + * @param protocol the Transport protocol + * @return a Transport object + * @exception NoSuchProviderException If provider for the given + * protocol is not found. + */ + public Transport getTransport(String protocol) + throws NoSuchProviderException { + return getTransport(new URLName(protocol, null, -1, null, null, null)); + } + + + /** + * Get a Transport object for the given URLName. If the requested + * Transport object cannot be obtained, NoSuchProviderException is thrown. + * + * The "scheme" part of the URL string (Refer RFC 1738) is used + * to locate the Transport protocol.

+ * + * @param url URLName that represents the desired Transport + * @return a closed Transport object + * @see javax.mail.URLName + * @exception NoSuchProviderException If a provider for the given + * URLName is not found. + */ + public Transport getTransport(URLName url) throws NoSuchProviderException { + String protocol = url.getProtocol(); + Provider p = getProvider(protocol); + return getTransport(p, url); + } + + /** + * Get an instance of the transport specified in the Provider. Instantiates + * the transport and returns it. + * + * @param provider Transport Provider that will be instantiated + * @return Instantiated Transport + * @exception NoSuchProviderException If provider for the given + * provider is not found. + */ + public Transport getTransport(Provider provider) + throws NoSuchProviderException { + return getTransport(provider, null); + } + + /** + * Get a Transport object that can transport a Message of the + * specified address type. + * + * @param address an address for which a Transport is needed + * @return A Transport object + * @see javax.mail.Address + * @exception NoSuchProviderException If provider for the + * Address type is not found + */ + public Transport getTransport(Address address) + throws NoSuchProviderException { + + String transportProtocol; + transportProtocol = + getProperty("mail.transport.protocol." + address.getType()); + if (transportProtocol != null) + return getTransport(transportProtocol); + transportProtocol = (String)addressMap.get(address.getType()); + if (transportProtocol != null) + return getTransport(transportProtocol); + throw new NoSuchProviderException("No provider for Address type: "+ + address.getType()); + } + + /** + * Get a Transport object using the given provider and urlname. + * + * @param provider the provider to use + * @param url urlname to use (can be null) + * @return A Transport object + * @exception NoSuchProviderException If no provider or the provider + * was the wrong class. + */ + + private Transport getTransport(Provider provider, URLName url) + throws NoSuchProviderException { + // make sure we have the correct type of provider + if (provider == null || provider.getType() != Provider.Type.TRANSPORT) { + throw new NoSuchProviderException("invalid provider"); + } + + return getService(provider, url, Transport.class); + } + + /** + * Get a Service object. Needs a provider object, but will + * create a URLName if needed. It attempts to instantiate + * the correct class. + * + * @param provider which provider to use + * @param url which URLName to use (can be null) + * @param type the service type (class) + * @exception NoSuchProviderException thrown when the class cannot be + * found or when it does not have the correct constructor + * (Session, URLName), or if it is not derived from + * Service. + */ + private T getService(Provider provider, URLName url, + Class type) + throws NoSuchProviderException { + // need a provider and url + if (provider == null) { + throw new NoSuchProviderException("null"); + } + + // create a url if needed + if (url == null) { + url = new URLName(provider.getProtocol(), null, -1, + null, null, null); + } + + Object service = null; + + // get the ClassLoader associated with the Authenticator + ClassLoader cl; + if (authenticator != null) + cl = authenticator.getClass().getClassLoader(); + else + cl = this.getClass().getClassLoader(); + + // now load the class + Class serviceClass = null; + try { + // First try the "application's" class loader. + ClassLoader ccl = getContextClassLoader(); + if (ccl != null) + try { + serviceClass = + Class.forName(provider.getClassName(), false, ccl); + } catch (ClassNotFoundException ex) { + // ignore it + } + if (serviceClass == null || !type.isAssignableFrom(serviceClass)) + serviceClass = + Class.forName(provider.getClassName(), false, cl); + + if (!type.isAssignableFrom(serviceClass)) + throw new ClassCastException( + type.getName() + " " + serviceClass.getName()); + } catch (Exception ex1) { + // That didn't work, now try the "system" class loader. + // (Need both of these because JDK 1.1 class loaders + // may not delegate to their parent class loader.) + try { + serviceClass = Class.forName(provider.getClassName()); + if (!type.isAssignableFrom(serviceClass)) + throw new ClassCastException( + type.getName() + " " + serviceClass.getName()); + } catch (Exception ex) { + // Nothing worked, give up. + logger.log(Level.FINE, "Exception loading provider", ex); + throw new NoSuchProviderException(provider.getProtocol()); + } + } + + // construct an instance of the class + try { + Class[] c = {javax.mail.Session.class, javax.mail.URLName.class}; + Constructor cons = serviceClass.getConstructor(c); + + Object[] o = {this, url}; + service = cons.newInstance(o); + + } catch (Exception ex) { + logger.log(Level.FINE, "Exception loading provider", ex); + throw new NoSuchProviderException(provider.getProtocol()); + } + + return type.cast(service); + } + + /** + * Save a PasswordAuthentication for this (store or transport) URLName. + * If pw is null the entry corresponding to the URLName is removed. + *

+ * This is normally used only by the store or transport implementations + * to allow authentication information to be shared among multiple + * uses of a session. + * + * @param url the URLName + * @param pw the PasswordAuthentication to save + */ + public void setPasswordAuthentication(URLName url, + PasswordAuthentication pw) { + if (pw == null) + authTable.remove(url); + else + authTable.put(url, pw); + } + + /** + * Return any saved PasswordAuthentication for this (store or transport) + * URLName. Normally used only by store or transport implementations. + * + * @param url the URLName + * @return the PasswordAuthentication corresponding to the URLName + */ + public PasswordAuthentication getPasswordAuthentication(URLName url) { + return authTable.get(url); + } + + /** + * Call back to the application to get the needed user name and password. + * The application should put up a dialog something like: + *

+     * Connecting to <protocol> mail service on host <addr>, port <port>.
+     * <prompt>
+     *
+     * User Name: <defaultUserName>
+     * Password:
+     * 
+ * + * @param addr InetAddress of the host. may be null. + * @param port the port on the host + * @param protocol protocol scheme (e.g. imap, pop3, etc.) + * @param prompt any additional String to show as part of + * the prompt; may be null. + * @param defaultUserName the default username. may be null. + * @return the authentication which was collected by the authenticator; + * may be null. + */ + public PasswordAuthentication requestPasswordAuthentication( + InetAddress addr, int port, + String protocol, String prompt, String defaultUserName) { + + if (authenticator != null) { + return authenticator.requestPasswordAuthentication( + addr, port, protocol, prompt, defaultUserName); + } else { + return null; + } + } + + /** + * Returns the Properties object associated with this Session + * + * @return Properties object + */ + public Properties getProperties() { + return props; + } + + /** + * Returns the value of the specified property. Returns null + * if this property does not exist. + * + * @param name the property name + * @return String that is the property value + */ + public String getProperty(String name) { + return props.getProperty(name); + } + + /** + * Load the protocol providers config files. + */ + private void loadProviders(Class cl) { + StreamLoader loader = new StreamLoader() { + @Override + public void load(InputStream is) throws IOException { + loadProvidersFromStream(is); + } + }; + + // load system-wide javamail.providers from the + // /{conf,lib} directory + try { + if (confDir != null) + loadFile(confDir + "javamail.providers", loader); + } catch (SecurityException ex) {} + + // next, add all the non-default services + ServiceLoader sl = ServiceLoader.load(Provider.class); + for (Provider p : sl) { + if (!p.getClass().isAnnotationPresent(DefaultProvider.class)) + addProvider(p); + } + + // load the META-INF/javamail.providers file supplied by an application + loadAllResources("META-INF/javamail.providers", cl, loader); + + // load default META-INF/javamail.default.providers from mail.jar file + loadResource("/META-INF/javamail.default.providers", cl, loader, false); + + // finally, add all the default services + sl = ServiceLoader.load(Provider.class); + for (Provider p : sl) { + if (p.getClass().isAnnotationPresent(DefaultProvider.class)) + addProvider(p); + } + + /* + * If we haven't loaded any providers, fake it. + */ + if (providers.size() == 0) { + logger.config("failed to load any providers, using defaults"); + // failed to load any providers, initialize with our defaults + addProvider(new Provider(Provider.Type.STORE, + "imap", "com.sun.mail.imap.IMAPStore", + "Oracle", Version.version)); + addProvider(new Provider(Provider.Type.STORE, + "imaps", "com.sun.mail.imap.IMAPSSLStore", + "Oracle", Version.version)); + addProvider(new Provider(Provider.Type.STORE, + "pop3", "com.sun.mail.pop3.POP3Store", + "Oracle", Version.version)); + addProvider(new Provider(Provider.Type.STORE, + "pop3s", "com.sun.mail.pop3.POP3SSLStore", + "Oracle", Version.version)); + addProvider(new Provider(Provider.Type.TRANSPORT, + "smtp", "com.sun.mail.smtp.SMTPTransport", + "Oracle", Version.version)); + addProvider(new Provider(Provider.Type.TRANSPORT, + "smtps", "com.sun.mail.smtp.SMTPSSLTransport", + "Oracle", Version.version)); + } + + if (logger.isLoggable(Level.CONFIG)) { + // dump the output of the tables for debugging + logger.config("Tables of loaded providers"); + logger.config("Providers Listed By Class Name: " + + providersByClassName.toString()); + logger.config("Providers Listed By Protocol: " + + providersByProtocol.toString()); + } + } + + private void loadProvidersFromStream(InputStream is) + throws IOException { + if (is != null) { + LineInputStream lis = new LineInputStream(is); + String currLine; + + // load and process one line at a time using LineInputStream + while ((currLine = lis.readLine()) != null) { + + if (currLine.startsWith("#")) + continue; + if (currLine.trim().length() == 0) + continue; // skip blank line + Provider.Type type = null; + String protocol = null, className = null; + String vendor = null, version = null; + + // separate line into key-value tuples + StringTokenizer tuples = new StringTokenizer(currLine,";"); + while (tuples.hasMoreTokens()) { + String currTuple = tuples.nextToken().trim(); + + // set the value of each attribute based on its key + int sep = currTuple.indexOf("="); + if (currTuple.startsWith("protocol=")) { + protocol = currTuple.substring(sep+1); + } else if (currTuple.startsWith("type=")) { + String strType = currTuple.substring(sep+1); + if (strType.equalsIgnoreCase("store")) { + type = Provider.Type.STORE; + } else if (strType.equalsIgnoreCase("transport")) { + type = Provider.Type.TRANSPORT; + } + } else if (currTuple.startsWith("class=")) { + className = currTuple.substring(sep+1); + } else if (currTuple.startsWith("vendor=")) { + vendor = currTuple.substring(sep+1); + } else if (currTuple.startsWith("version=")) { + version = currTuple.substring(sep+1); + } + } + + // check if a valid Provider; else, continue + if (type == null || protocol == null || className == null + || protocol.length() <= 0 || className.length() <= 0) { + + logger.log(Level.CONFIG, "Bad provider entry: {0}", + currLine); + continue; + } + Provider provider = new Provider(type, protocol, className, + vendor, version); + + // add the newly-created Provider to the lookup tables + addProvider(provider); + } + } + } + + /** + * Add a provider to the session. + * + * @param provider the provider to add + * @since JavaMail 1.4 + */ + public synchronized void addProvider(Provider provider) { + providers.add(provider); + providersByClassName.put(provider.getClassName(), provider); + if (!providersByProtocol.containsKey(provider.getProtocol())) + providersByProtocol.put(provider.getProtocol(), provider); + } + + // load maps in reverse order of preference so that the preferred + // map is loaded last since its entries will override the previous ones + private void loadAddressMap(Class cl) { + StreamLoader loader = new StreamLoader() { + @Override + public void load(InputStream is) throws IOException { + addressMap.load(is); + } + }; + + // load default META-INF/javamail.default.address.map from mail.jar + loadResource("/META-INF/javamail.default.address.map", cl, loader, true); + + // load the META-INF/javamail.address.map file supplied by an app + loadAllResources("META-INF/javamail.address.map", cl, loader); + + // load system-wide javamail.address.map from the + // /{conf,lib} directory + try { + if (confDir != null) + loadFile(confDir + "javamail.address.map", loader); + } catch (SecurityException ex) {} + + if (addressMap.isEmpty()) { + logger.config("failed to load address map, using defaults"); + addressMap.put("rfc822", "smtp"); + } + } + + /** + * Set the default transport protocol to use for addresses of + * the specified type. Normally the default is set by the + * javamail.default.address.map or + * javamail.address.map files or resources. + * + * @param addresstype type of address + * @param protocol name of protocol + * @see #getTransport(Address) + * @since JavaMail 1.4 + */ + public synchronized void setProtocolForAddress(String addresstype, + String protocol) { + if (protocol == null) + addressMap.remove(addresstype); + else + addressMap.put(addresstype, protocol); + } + + /** + * Load from the named file. + */ + private void loadFile(String name, StreamLoader loader) { + InputStream clis = null; + try { + clis = new BufferedInputStream(new FileInputStream(name)); + loader.load(clis); + logger.log(Level.CONFIG, "successfully loaded file: {0}", name); + } catch (FileNotFoundException fex) { + // ignore it + } catch (IOException e) { + if (logger.isLoggable(Level.CONFIG)) + logger.log(Level.CONFIG, "not loading file: " + name, e); + } catch (SecurityException sex) { + if (logger.isLoggable(Level.CONFIG)) + logger.log(Level.CONFIG, "not loading file: " + name, sex); + } finally { + try { + if (clis != null) + clis.close(); + } catch (IOException ex) { } // ignore it + } + } + + /** + * Load from the named resource. + */ + private void loadResource(String name, Class cl, StreamLoader loader, + boolean expected) { + InputStream clis = null; + try { + clis = getResourceAsStream(cl, name); + if (clis != null) { + loader.load(clis); + logger.log(Level.CONFIG, "successfully loaded resource: {0}", + name); + } else { + if (expected) + logger.log(Level.WARNING, + "expected resource not found: {0}", name); + } + } catch (IOException e) { + logger.log(Level.CONFIG, "Exception loading resource", e); + } catch (SecurityException sex) { + logger.log(Level.CONFIG, "Exception loading resource", sex); + } finally { + try { + if (clis != null) + clis.close(); + } catch (IOException ex) { } // ignore it + } + } + + /** + * Load all of the named resource. + */ + private void loadAllResources(String name, Class cl, + StreamLoader loader) { + boolean anyLoaded = false; + try { + URL[] urls; + ClassLoader cld = null; + // First try the "application's" class loader. + cld = getContextClassLoader(); + if (cld == null) + cld = cl.getClassLoader(); + if (cld != null) + urls = getResources(cld, name); + else + urls = getSystemResources(name); + if (urls != null) { + for (int i = 0; i < urls.length; i++) { + URL url = urls[i]; + InputStream clis = null; + logger.log(Level.CONFIG, "URL {0}", url); + try { + clis = openStream(url); + if (clis != null) { + loader.load(clis); + anyLoaded = true; + logger.log(Level.CONFIG, + "successfully loaded resource: {0}", url); + } else { + logger.log(Level.CONFIG, + "not loading resource: {0}", url); + } + } catch (FileNotFoundException fex) { + // ignore it + } catch (IOException ioex) { + logger.log(Level.CONFIG, "Exception loading resource", + ioex); + } catch (SecurityException sex) { + logger.log(Level.CONFIG, "Exception loading resource", + sex); + } finally { + try { + if (clis != null) + clis.close(); + } catch (IOException cex) { } + } + } + } + } catch (Exception ex) { + logger.log(Level.CONFIG, "Exception loading resource", ex); + } + + // if failed to load anything, fall back to old technique, just in case + if (!anyLoaded) { + /* + logger.config("!anyLoaded"); + */ + loadResource("/" + name, cl, loader, false); + } + } + + /* + * Following are security related methods that work on JDK 1.2 or newer. + */ + + static ClassLoader getContextClassLoader() { + return AccessController.doPrivileged( + new PrivilegedAction() { + @Override + public ClassLoader run() { + ClassLoader cl = null; + try { + cl = Thread.currentThread().getContextClassLoader(); + } catch (SecurityException ex) { + } + return cl; + } + } + ); + } + + private static InputStream getResourceAsStream(final Class c, + final String name) throws IOException { + try { + return AccessController.doPrivileged( + new PrivilegedExceptionAction() { + @Override + public InputStream run() throws IOException { + try { + return c.getResourceAsStream(name); + } catch (RuntimeException e) { + // gracefully handle ClassLoader bugs (Tomcat) + IOException ioex = new IOException( + "ClassLoader.getResourceAsStream failed"); + ioex.initCause(e); + throw ioex; + } + } + } + ); + } catch (PrivilegedActionException e) { + throw (IOException)e.getException(); + } + } + + private static URL[] getResources(final ClassLoader cl, final String name) { + return AccessController.doPrivileged(new PrivilegedAction() { + @Override + public URL[] run() { + URL[] ret = null; + try { + List v = Collections.list(cl.getResources(name)); + if (!v.isEmpty()) { + ret = new URL[v.size()]; + v.toArray(ret); + } + } catch (IOException ioex) { + } catch (SecurityException ex) { } + return ret; + } + }); + } + + private static URL[] getSystemResources(final String name) { + return AccessController.doPrivileged(new PrivilegedAction() { + @Override + public URL[] run() { + URL[] ret = null; + try { + List v = Collections.list( + ClassLoader.getSystemResources(name)); + if (!v.isEmpty()) { + ret = new URL[v.size()]; + v.toArray(ret); + } + } catch (IOException ioex) { + } catch (SecurityException ex) { } + return ret; + } + }); + } + + private static InputStream openStream(final URL url) throws IOException { + try { + return AccessController.doPrivileged( + new PrivilegedExceptionAction() { + @Override + public InputStream run() throws IOException { + return url.openStream(); + } + } + ); + } catch (PrivilegedActionException e) { + throw (IOException)e.getException(); + } + } + + EventQueue getEventQueue() { + return q; + } +} + +/** + * Support interface to generalize + * code that loads resources from stream. + */ +interface StreamLoader { + public void load(InputStream is) throws IOException; +} diff --git a/app/src/main/java/javax/mail/Store.java b/app/src/main/java/javax/mail/Store.java new file mode 100644 index 0000000000..2678a09e57 --- /dev/null +++ b/app/src/main/java/javax/mail/Store.java @@ -0,0 +1,301 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.io.*; +import java.net.*; +import java.util.*; +import javax.mail.event.*; + +/** + * An abstract class that models a message store and its + * access protocol, for storing and retrieving messages. + * Subclasses provide actual implementations.

+ * + * Note that Store extends the Service + * class, which provides many common methods for naming stores, + * connecting to stores, and listening to connection events. + * + * @author John Mani + * @author Bill Shannon + * + * @see javax.mail.Service + * @see javax.mail.event.ConnectionEvent + * @see javax.mail.event.StoreEvent + */ + +public abstract class Store extends Service { + + /** + * Constructor. + * + * @param session Session object for this Store. + * @param urlname URLName object to be used for this Store + */ + protected Store(Session session, URLName urlname) { + super(session, urlname); + } + + /** + * Returns a Folder object that represents the 'root' of + * the default namespace presented to the user by the Store. + * + * @return the root Folder + * @exception IllegalStateException if this Store is not connected. + * @exception MessagingException for other failures + */ + public abstract Folder getDefaultFolder() throws MessagingException; + + /** + * Return the Folder object corresponding to the given name. Note + * that a Folder object is returned even if the named folder does + * not physically exist on the Store. The exists() + * method on the folder object indicates whether this folder really + * exists.

+ * + * Folder objects are not cached by the Store, so invoking this + * method on the same name multiple times will return that many + * distinct Folder objects. + * + * @param name The name of the Folder. In some Stores, name can + * be an absolute path if it starts with the + * hierarchy delimiter. Else it is interpreted + * relative to the 'root' of this namespace. + * @return Folder object + * @exception IllegalStateException if this Store is not connected. + * @exception MessagingException for other failures + * @see Folder#exists + * @see Folder#create + */ + public abstract Folder getFolder(String name) + throws MessagingException; + + /** + * Return a closed Folder object, corresponding to the given + * URLName. The store specified in the given URLName should + * refer to this Store object.

+ * + * Implementations of this method may obtain the name of the + * actual folder using the getFile() method on + * URLName, and use that name to create the folder. + * + * @param url URLName that denotes a folder + * @return Folder object + * @exception IllegalStateException if this Store is not connected. + * @exception MessagingException for other failures + * @see URLName + */ + public abstract Folder getFolder(URLName url) + throws MessagingException; + + /** + * Return a set of folders representing the personal namespaces + * for the current user. A personal namespace is a set of names that + * is considered within the personal scope of the authenticated user. + * Typically, only the authenticated user has access to mail folders + * in their personal namespace. If an INBOX exists for a user, it + * must appear within the user's personal namespace. In the + * typical case, there should be only one personal namespace for each + * user in each Store.

+ * + * This implementation returns an array with a single entry containing + * the return value of the getDefaultFolder method. + * Subclasses should override this method to return appropriate information. + * + * @return array of Folder objects + * @exception IllegalStateException if this Store is not connected. + * @exception MessagingException for other failures + * @since JavaMail 1.2 + */ + public Folder[] getPersonalNamespaces() throws MessagingException { + return new Folder[] { getDefaultFolder() }; + } + + /** + * Return a set of folders representing the namespaces for + * user. The namespaces returned represent the + * personal namespaces for the user. To access mail folders in the + * other user's namespace, the currently authenticated user must be + * explicitly granted access rights. For example, it is common for + * a manager to grant to their secretary access rights to their + * mail folders.

+ * + * This implementation returns an empty array. Subclasses should + * override this method to return appropriate information. + * + * @param user the user name + * @return array of Folder objects + * @exception IllegalStateException if this Store is not connected. + * @exception MessagingException for other failures + * @since JavaMail 1.2 + */ + public Folder[] getUserNamespaces(String user) + throws MessagingException { + return new Folder[0]; + } + + /** + * Return a set of folders representing the shared namespaces. + * A shared namespace is a namespace that consists of mail folders + * that are intended to be shared amongst users and do not exist + * within a user's personal namespace.

+ * + * This implementation returns an empty array. Subclasses should + * override this method to return appropriate information. + * + * @exception IllegalStateException if this Store is not connected. + * @exception MessagingException for other failures + * @return array of Folder objects + * @since JavaMail 1.2 + */ + public Folder[] getSharedNamespaces() throws MessagingException { + return new Folder[0]; + } + + // Vector of Store listeners + private volatile Vector storeListeners = null; + + /** + * Add a listener for StoreEvents on this Store.

+ * + * The default implementation provided here adds this listener + * to an internal list of StoreListeners. + * + * @param l the Listener for Store events + * @see javax.mail.event.StoreEvent + */ + public synchronized void addStoreListener(StoreListener l) { + if (storeListeners == null) + storeListeners = new Vector<>(); + storeListeners.addElement(l); + } + + /** + * Remove a listener for Store events.

+ * + * The default implementation provided here removes this listener + * from the internal list of StoreListeners. + * + * @param l the listener + * @see #addStoreListener + */ + public synchronized void removeStoreListener(StoreListener l) { + if (storeListeners != null) + storeListeners.removeElement(l); + } + + /** + * Notify all StoreListeners. Store implementations are + * expected to use this method to broadcast StoreEvents.

+ * + * The provided default implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * StoreListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param type the StoreEvent type + * @param message a message for the StoreEvent + */ + protected void notifyStoreListeners(int type, String message) { + if (storeListeners == null) + return; + + StoreEvent e = new StoreEvent(this, type, message); + queueEvent(e, storeListeners); + } + + // Vector of folder listeners + private volatile Vector folderListeners = null; + + /** + * Add a listener for Folder events on any Folder object + * obtained from this Store. FolderEvents are delivered to + * FolderListeners on the affected Folder as well as to + * FolderListeners on the containing Store.

+ * + * The default implementation provided here adds this listener + * to an internal list of FolderListeners. + * + * @param l the Listener for Folder events + * @see javax.mail.event.FolderEvent + */ + public synchronized void addFolderListener(FolderListener l) { + if (folderListeners == null) + folderListeners = new Vector<>(); + folderListeners.addElement(l); + } + + /** + * Remove a listener for Folder events.

+ * + * The default implementation provided here removes this listener + * from the internal list of FolderListeners. + * + * @param l the listener + * @see #addFolderListener + */ + public synchronized void removeFolderListener(FolderListener l) { + if (folderListeners != null) + folderListeners.removeElement(l); + } + + /** + * Notify all FolderListeners. Store implementations are + * expected to use this method to broadcast Folder events.

+ * + * The provided default implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * FolderListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param type type of FolderEvent + * @param folder affected Folder + * @see #notifyFolderRenamedListeners + */ + protected void notifyFolderListeners(int type, Folder folder) { + if (folderListeners == null) + return; + + FolderEvent e = new FolderEvent(this, folder, type); + queueEvent(e, folderListeners); + } + + /** + * Notify all FolderListeners about the renaming of a folder. + * Store implementations are expected to use this method to broadcast + * Folder events indicating the renaming of folders.

+ * + * The provided default implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * FolderListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param oldF the folder being renamed + * @param newF the folder representing the new name. + * @since JavaMail 1.1 + */ + protected void notifyFolderRenamedListeners(Folder oldF, Folder newF) { + if (folderListeners == null) + return; + + FolderEvent e = new FolderEvent(this, oldF, newF,FolderEvent.RENAMED); + queueEvent(e, folderListeners); + } +} diff --git a/app/src/main/java/javax/mail/StoreClosedException.java b/app/src/main/java/javax/mail/StoreClosedException.java new file mode 100644 index 0000000000..29a0bbb722 --- /dev/null +++ b/app/src/main/java/javax/mail/StoreClosedException.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * This exception is thrown when a method is invoked on a Messaging object + * and the Store that owns that object has died due to some reason. + * This exception should be treated as a fatal error; in particular any + * messaging object belonging to that Store must be considered invalid.

+ * + * The connect method may be invoked on the dead Store object to + * revive it.

+ * + * The getMessage() method returns more detailed information about the + * error that caused this exception.

+ * + * @author John Mani + */ + +public class StoreClosedException extends MessagingException { + transient private Store store; + + private static final long serialVersionUID = -3145392336120082655L; + + /** + * Constructs a StoreClosedException with no detail message. + * + * @param store The dead Store object + */ + public StoreClosedException(Store store) { + this(store, null); + } + + /** + * Constructs a StoreClosedException with the specified + * detail message. + * + * @param store The dead Store object + * @param message The detailed error message + */ + public StoreClosedException(Store store, String message) { + super(message); + this.store = store; + } + + /** + * Constructs a StoreClosedException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param store The dead Store object + * @param message The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public StoreClosedException(Store store, String message, Exception e) { + super(message, e); + this.store = store; + } + + /** + * Returns the dead Store object. + * + * @return the dead Store object + */ + public Store getStore() { + return store; + } +} diff --git a/app/src/main/java/javax/mail/Transport.java b/app/src/main/java/javax/mail/Transport.java new file mode 100644 index 0000000000..e9b3029102 --- /dev/null +++ b/app/src/main/java/javax/mail/Transport.java @@ -0,0 +1,400 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Vector; +import javax.mail.event.*; + +/** + * An abstract class that models a message transport. + * Subclasses provide actual implementations.

+ * + * Note that Transport extends the Service + * class, which provides many common methods for naming transports, + * connecting to transports, and listening to connection events. + * + * @author John Mani + * @author Max Spivak + * @author Bill Shannon + * + * @see javax.mail.Service + * @see javax.mail.event.ConnectionEvent + * @see javax.mail.event.TransportEvent + */ + +public abstract class Transport extends Service { + + /** + * Constructor. + * + * @param session Session object for this Transport. + * @param urlname URLName object to be used for this Transport + */ + public Transport(Session session, URLName urlname) { + super(session, urlname); + } + + /** + * Send a message. The message will be sent to all recipient + * addresses specified in the message (as returned from the + * Message method getAllRecipients), + * using message transports appropriate to each address. The + * send method calls the saveChanges + * method on the message before sending it.

+ * + * If any of the recipient addresses is detected to be invalid by + * the Transport during message submission, a SendFailedException + * is thrown. Clients can get more detail about the failure by examining + * the exception. Whether or not the message is still sent successfully + * to any valid addresses depends on the Transport implementation. See + * SendFailedException for more details. Note also that success does + * not imply that the message was delivered to the ultimate recipient, + * as failures may occur in later stages of delivery. Once a Transport + * accepts a message for delivery to a recipient, failures that occur later + * should be reported to the user via another mechanism, such as + * returning the undeliverable message.

+ * + * In typical usage, a SendFailedException reflects an error detected + * by the server. The details of the SendFailedException will usually + * contain the error message from the server (such as an SMTP error + * message). An address may be detected as invalid for a variety of + * reasons - the address may not exist, the address may have invalid + * syntax, the address may have exceeded its quota, etc.

+ * + * Note that send is a static method that creates and + * manages its own connection. Any connection associated with any + * Transport instance used to invoke this method is ignored and not + * used. This method should only be invoked using the form + * Transport.send(msg);, and should never be invoked + * using an instance variable. + * + * @param msg the message to send + * @exception SendFailedException if the message could not + * be sent to some or any of the recipients. + * @exception MessagingException for other failures + * @see Message#saveChanges + * @see Message#getAllRecipients + * @see #send(Message, Address[]) + * @see javax.mail.SendFailedException + */ + public static void send(Message msg) throws MessagingException { + msg.saveChanges(); // do this first + send0(msg, msg.getAllRecipients(), null, null); + } + + /** + * Send the message to the specified addresses, ignoring any + * recipients specified in the message itself. The + * send method calls the saveChanges + * method on the message before sending it.

+ * + * @param msg the message to send + * @param addresses the addresses to which to send the message + * @exception SendFailedException if the message could not + * be sent to some or any of the recipients. + * @exception MessagingException for other failures + * @see Message#saveChanges + * @see #send(Message) + * @see javax.mail.SendFailedException + */ + public static void send(Message msg, Address[] addresses) + throws MessagingException { + + msg.saveChanges(); + send0(msg, addresses, null, null); + } + + /** + * Send a message. The message will be sent to all recipient + * addresses specified in the message (as returned from the + * Message method getAllRecipients). + * The send method calls the saveChanges + * method on the message before sending it.

+ * + * Use the specified user name and password to authenticate to + * the mail server. + * + * @param msg the message to send + * @param user the user name + * @param password this user's password + * @exception SendFailedException if the message could not + * be sent to some or any of the recipients. + * @exception MessagingException for other failures + * @see Message#saveChanges + * @see #send(Message) + * @see javax.mail.SendFailedException + * @since JavaMail 1.5 + */ + public static void send(Message msg, + String user, String password) throws MessagingException { + + msg.saveChanges(); + send0(msg, msg.getAllRecipients(), user, password); + } + + /** + * Send the message to the specified addresses, ignoring any + * recipients specified in the message itself. The + * send method calls the saveChanges + * method on the message before sending it.

+ * + * Use the specified user name and password to authenticate to + * the mail server. + * + * @param msg the message to send + * @param addresses the addresses to which to send the message + * @param user the user name + * @param password this user's password + * @exception SendFailedException if the message could not + * be sent to some or any of the recipients. + * @exception MessagingException for other failures + * @see Message#saveChanges + * @see #send(Message) + * @see javax.mail.SendFailedException + * @since JavaMail 1.5 + */ + public static void send(Message msg, Address[] addresses, + String user, String password) throws MessagingException { + + msg.saveChanges(); + send0(msg, addresses, user, password); + } + + // send, but without the saveChanges + private static void send0(Message msg, Address[] addresses, + String user, String password) throws MessagingException { + + if (addresses == null || addresses.length == 0) + throw new SendFailedException("No recipient addresses"); + + /* + * protocols is a map containing the addresses + * indexed by address type + */ + Map> protocols + = new HashMap<>(); + + // Lists of addresses + List

invalid = new ArrayList<>(); + List
validSent = new ArrayList<>(); + List
validUnsent = new ArrayList<>(); + + for (int i = 0; i < addresses.length; i++) { + // is this address type already in the map? + if (protocols.containsKey(addresses[i].getType())) { + List
v = protocols.get(addresses[i].getType()); + v.add(addresses[i]); + } else { + // need to add a new protocol + List
w = new ArrayList<>(); + w.add(addresses[i]); + protocols.put(addresses[i].getType(), w); + } + } + + int dsize = protocols.size(); + if (dsize == 0) + throw new SendFailedException("No recipient addresses"); + + Session s = (msg.session != null) ? msg.session : + Session.getDefaultInstance(System.getProperties(), null); + Transport transport; + + /* + * Optimize the case of a single protocol. + */ + if (dsize == 1) { + transport = s.getTransport(addresses[0]); + try { + if (user != null) + transport.connect(user, password); + else + transport.connect(); + transport.sendMessage(msg, addresses); + } finally { + transport.close(); + } + return; + } + + /* + * More than one protocol. Have to do them one at a time + * and collect addresses and chain exceptions. + */ + MessagingException chainedEx = null; + boolean sendFailed = false; + + for(List
v : protocols.values()) { + Address[] protaddresses = new Address[v.size()]; + v.toArray(protaddresses); + + // Get a Transport that can handle this address type. + if ((transport = s.getTransport(protaddresses[0])) == null) { + // Could not find an appropriate Transport .. + // Mark these addresses invalid. + for (int j = 0; j < protaddresses.length; j++) + invalid.add(protaddresses[j]); + continue; + } + try { + transport.connect(); + transport.sendMessage(msg, protaddresses); + } catch (SendFailedException sex) { + sendFailed = true; + // chain the exception we're catching to any previous ones + if (chainedEx == null) + chainedEx = sex; + else + chainedEx.setNextException(sex); + + // retrieve invalid addresses + Address[] a = sex.getInvalidAddresses(); + if (a != null) + for (int j = 0; j < a.length; j++) + invalid.add(a[j]); + + // retrieve validSent addresses + a = sex.getValidSentAddresses(); + if (a != null) + for (int k = 0; k < a.length; k++) + validSent.add(a[k]); + + // retrieve validUnsent addresses + Address[] c = sex.getValidUnsentAddresses(); + if (c != null) + for (int l = 0; l < c.length; l++) + validUnsent.add(c[l]); + } catch (MessagingException mex) { + sendFailed = true; + // chain the exception we're catching to any previous ones + if (chainedEx == null) + chainedEx = mex; + else + chainedEx.setNextException(mex); + } finally { + transport.close(); + } + } + + // done with all protocols. throw exception if something failed + if (sendFailed || invalid.size() != 0 || validUnsent.size() != 0) { + Address[] a = null, b = null, c = null; + + // copy address lists into arrays + if (validSent.size() > 0) { + a = new Address[validSent.size()]; + validSent.toArray(a); + } + if (validUnsent.size() > 0) { + b = new Address[validUnsent.size()]; + validUnsent.toArray(b); + } + if (invalid.size() > 0) { + c = new Address[invalid.size()]; + invalid.toArray(c); + } + throw new SendFailedException("Sending failed", chainedEx, + a, b, c); + } + } + + /** + * Send the Message to the specified list of addresses. An appropriate + * TransportEvent indicating the delivery status is delivered to any + * TransportListener registered on this Transport. Also, if any of + * the addresses is invalid, a SendFailedException is thrown. + * Whether or not the message is still sent succesfully to + * any valid addresses depends on the Transport implementation.

+ * + * Unlike the static send method, the sendMessage + * method does not call the saveChanges method on + * the message; the caller should do so. + * + * @param msg The Message to be sent + * @param addresses array of addresses to send this message to + * @see javax.mail.event.TransportEvent + * @exception SendFailedException if the send failed because of + * invalid addresses. + * @exception MessagingException if the connection is dead or not in the + * connected state + */ + public abstract void sendMessage(Message msg, Address[] addresses) + throws MessagingException; + + // Vector of Transport listeners + private volatile Vector transportListeners = null; + + /** + * Add a listener for Transport events.

+ * + * The default implementation provided here adds this listener + * to an internal list of TransportListeners. + * + * @param l the Listener for Transport events + * @see javax.mail.event.TransportEvent + */ + public synchronized void addTransportListener(TransportListener l) { + if (transportListeners == null) + transportListeners = new Vector<>(); + transportListeners.addElement(l); + } + + /** + * Remove a listener for Transport events.

+ * + * The default implementation provided here removes this listener + * from the internal list of TransportListeners. + * + * @param l the listener + * @see #addTransportListener + */ + public synchronized void removeTransportListener(TransportListener l) { + if (transportListeners != null) + transportListeners.removeElement(l); + } + + /** + * Notify all TransportListeners. Transport implementations are + * expected to use this method to broadcast TransportEvents.

+ * + * The provided default implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * TransportListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param type the TransportEvent type + * @param validSent valid addresses to which message was sent + * @param validUnsent valid addresses to which message was not sent + * @param invalid the invalid addresses + * @param msg the message + */ + protected void notifyTransportListeners(int type, Address[] validSent, + Address[] validUnsent, + Address[] invalid, Message msg) { + if (transportListeners == null) + return; + + TransportEvent e = new TransportEvent(this, type, validSent, + validUnsent, invalid, msg); + queueEvent(e, transportListeners); + } +} diff --git a/app/src/main/java/javax/mail/UIDFolder.java b/app/src/main/java/javax/mail/UIDFolder.java new file mode 100644 index 0000000000..2a8b97c4c4 --- /dev/null +++ b/app/src/main/java/javax/mail/UIDFolder.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.util.NoSuchElementException; + +/** + * The UIDFolder interface is implemented by Folders + * that can support the "disconnected" mode of operation, by providing + * unique-ids for messages in the folder. This interface is based on + * the IMAP model for supporting disconnected operation.

+ * + * A Unique identifier (UID) is a positive long value, assigned to + * each message in a specific folder. Unique identifiers are assigned + * in a strictly ascending fashion in the mailbox. + * That is, as each message is added to the mailbox it is assigned a + * higher UID than the message(s) which were added previously. Unique + * identifiers persist across sessions. This permits a client to + * resynchronize its state from a previous session with the server.

+ * + * Associated with every mailbox is a unique identifier validity value. + * If unique identifiers from an earlier session fail to persist to + * this session, the unique identifier validity value + * must be greater than the one used in the earlier + * session.

+ * + * Refer to RFC 2060 + * for more information. + * + * All the Folder objects returned by the default IMAP provider implement + * the UIDFolder interface. Use it as follows: + *

+ *
+ * 	Folder f = store.getFolder("whatever");
+ *	UIDFolder uf = (UIDFolder)f;
+ *	long uid = uf.getUID(msg);
+ *
+ * 

+ * + * @author Bill Shannon + * @author John Mani + */ + +public interface UIDFolder { + + /** + * A fetch profile item for fetching UIDs. + * This inner class extends the FetchProfile.Item + * class to add new FetchProfile item types, specific to UIDFolders. + * The only item currently defined here is the UID item. + * + * @see FetchProfile + */ + public static class FetchProfileItem extends FetchProfile.Item { + protected FetchProfileItem(String name) { + super(name); + } + + /** + * UID is a fetch profile item that can be included in a + * FetchProfile during a fetch request to a Folder. + * This item indicates that the UIDs for messages in the specified + * range are desired to be prefetched.

+ * + * An example of how a client uses this is below: + *

+	 *
+	 * 	FetchProfile fp = new FetchProfile();
+	 *	fp.add(UIDFolder.FetchProfileItem.UID);
+	 *	folder.fetch(msgs, fp);
+	 *
+	 * 
+ */ + public static final FetchProfileItem UID = + new FetchProfileItem("UID"); + } + + /** + * This is a special value that can be used as the end + * parameter in getMessagesByUID(start, end), to denote the + * UID of the last message in the folder. + * + * @see #getMessagesByUID + */ + public static final long LASTUID = -1; + + /** + * The largest value possible for a UID, a 32-bit unsigned integer. + * This can be used to fetch all new messages by keeping track of the + * last UID that was seen and using: + *
+     *
+     * 	Folder f = store.getFolder("whatever");
+     *	UIDFolder uf = (UIDFolder)f;
+     *	Message[] newMsgs =
+     *		uf.getMessagesByUID(lastSeenUID + 1, UIDFolder.MAXUID);
+     *
+     * 

+ * + * @since JavaMail 1.6 + */ + public static final long MAXUID = 0xffffffffL; // max 32-bit unsigned int + + /** + * Returns the UIDValidity value associated with this folder.

+ * + * Clients typically compare this value against a UIDValidity + * value saved from a previous session to insure that any cached + * UIDs are not stale. + * + * @return UIDValidity + * @exception MessagingException for failures + */ + public long getUIDValidity() throws MessagingException; + + /** + * Get the Message corresponding to the given UID. If no such + * message exists, null is returned. + * + * @param uid UID for the desired message + * @return the Message object. null is returned + * if no message corresponding to this UID is obtained. + * @exception MessagingException for failures + */ + public Message getMessageByUID(long uid) throws MessagingException; + + /** + * Get the Messages specified by the given range. The special + * value LASTUID can be used for the end parameter + * to indicate the UID of the last message in the folder.

+ * + * Note that end need not be greater than start; + * the order of the range doesn't matter. + * Note also that, unless the folder is empty, use of LASTUID ensures + * that at least one message will be returned - the last message in the + * folder. + * + * @param start start UID + * @param end end UID + * @return array of Message objects + * @exception MessagingException for failures + * @see #LASTUID + */ + public Message[] getMessagesByUID(long start, long end) + throws MessagingException; + + /** + * Get the Messages specified by the given array of UIDs. If any UID is + * invalid, null is returned for that entry.

+ * + * Note that the returned array will be of the same size as the specified + * array of UIDs, and null entries may be present in the + * array to indicate invalid UIDs. + * + * @param uids array of UIDs + * @return array of Message objects + * @exception MessagingException for failures + */ + public Message[] getMessagesByUID(long[] uids) + throws MessagingException; + + /** + * Get the UID for the specified message. Note that the message + * must belong to this folder. Otherwise + * java.util.NoSuchElementException is thrown. + * + * @param message Message from this folder + * @return UID for this message + * @exception NoSuchElementException if the given Message + * is not in this Folder. + * @exception MessagingException for other failures + */ + public long getUID(Message message) throws MessagingException; + + /** + * Returns the predicted UID that will be assigned to the + * next message that is appended to this folder. + * Messages might be appended to the folder after this value + * is retrieved, causing this value to be out of date. + * This value might only be updated when a folder is first opened. + * Note that messages may have been appended to the folder + * while it was open and thus this value may be out of + * date.

+ * + * If the value is unknown, -1 is returned.

+ * + * @return the UIDNEXT value, or -1 if unknown + * @exception MessagingException for failures + * @since JavaMail 1.6 + */ + public long getUIDNext() throws MessagingException; +} diff --git a/app/src/main/java/javax/mail/URLName.java b/app/src/main/java/javax/mail/URLName.java new file mode 100644 index 0000000000..f30a154715 --- /dev/null +++ b/app/src/main/java/javax/mail/URLName.java @@ -0,0 +1,786 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +import java.net.*; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStreamWriter; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.BitSet; +import java.util.Locale; + + +/** + * The name of a URL. This class represents a URL name and also + * provides the basic parsing functionality to parse most internet + * standard URL schemes.

+ * + * Note that this class differs from java.net.URL + * in that this class just represents the name of a URL, it does + * not model the connection to a URL. + * + * @author Christopher Cotton + * @author Bill Shannon + */ + +public class URLName { + + /** + * The full version of the URL + */ + protected String fullURL; + + /** + * The protocol to use (ftp, http, nntp, imap, pop3 ... etc.) . + */ + private String protocol; + + /** + * The username to use when connecting + */ + private String username; + + /** + * The password to use when connecting. + */ + private String password; + + /** + * The host name to which to connect. + */ + private String host; + + /** + * The host's IP address, used in equals and hashCode. + * Computed on demand. + */ + private InetAddress hostAddress; + private boolean hostAddressKnown = false; + + /** + * The protocol port to connect to. + */ + private int port = -1; + + /** + * The specified file name on that host. + */ + private String file; + + /** + * # reference. + */ + private String ref; + + /** + * Our hash code. + */ + private int hashCode = 0; + + /** + * A way to turn off encoding, just in case... + */ + private static boolean doEncode = true; + + static { + try { + doEncode = !Boolean.getBoolean("mail.URLName.dontencode"); + } catch (Exception ex) { + // ignore any errors + } + } + + /** + * Creates a URLName object from the specified protocol, + * host, port number, file, username, and password. Specifying a port + * number of -1 indicates that the URL should use the default port for + * the protocol. + * + * @param protocol the protocol + * @param host the host name + * @param port the port number + * @param file the file + * @param username the user name + * @param password the password + */ + public URLName( + String protocol, + String host, + int port, + String file, + String username, + String password + ) + { + this.protocol = protocol; + this.host = host; + this.port = port; + int refStart; + if (file != null && (refStart = file.indexOf('#')) != -1) { + this.file = file.substring(0, refStart); + this.ref = file.substring(refStart + 1); + } else { + this.file = file; + this.ref = null; + } + this.username = doEncode ? encode(username) : username; + this.password = doEncode ? encode(password) : password; + } + + /** + * Construct a URLName from a java.net.URL object. + * + * @param url the URL + */ + public URLName(URL url) { + this(url.toString()); + } + + /** + * Construct a URLName from the string. Parses out all the possible + * information (protocol, host, port, file, username, password). + * + * @param url the URL string + */ + public URLName(String url) { + parseString(url); + } + + /** + * Constructs a string representation of this URLName. + */ + @Override + public String toString() { + if (fullURL == null) { + // add the "protocol:" + StringBuilder tempURL = new StringBuilder(); + if (protocol != null) { + tempURL.append(protocol); + tempURL.append(":"); + } + + if (username != null || host != null) { + // add the "//" + tempURL.append("//"); + + // add the user:password@ + // XXX - can you just have a password? without a username? + if (username != null) { + tempURL.append(username); + + if (password != null){ + tempURL.append(":"); + tempURL.append(password); + } + + tempURL.append("@"); + } + + // add host + if (host != null) { + tempURL.append(host); + } + + // add port (if needed) + if (port != -1) { + tempURL.append(":"); + tempURL.append(Integer.toString(port)); + } + if (file != null) + tempURL.append("/"); + } + + // add the file + if (file != null) { + tempURL.append(file); + } + + // add the ref + if (ref != null) { + tempURL.append("#"); + tempURL.append(ref); + } + + // create the fullURL now + fullURL = tempURL.toString(); + } + + return fullURL; + } + + /** + * Method which does all of the work of parsing the string. + * + * @param url the URL string to parse + */ + protected void parseString(String url) { + // initialize everything in case called from subclass + // (URLName really should be a final class) + protocol = file = ref = host = username = password = null; + port = -1; + + int len = url.length(); + + // find the protocol + // XXX - should check for only legal characters before the colon + // (legal: a-z, A-Z, 0-9, "+", ".", "-") + int protocolEnd = url.indexOf(':'); + if (protocolEnd != -1) + protocol = url.substring(0, protocolEnd); + + // is this an Internet standard URL that contains a host name? + if (url.regionMatches(protocolEnd + 1, "//", 0, 2)) { + // find where the file starts + String fullhost = null; + int fileStart = url.indexOf('/', protocolEnd + 3); + if (fileStart != -1) { + fullhost = url.substring(protocolEnd + 3, fileStart); + if (fileStart + 1 < len) + file = url.substring(fileStart + 1); + else + file = ""; + } else + fullhost = url.substring(protocolEnd + 3); + + // examine the fullhost, for username password etc. + int i = fullhost.indexOf('@'); + if (i != -1) { + String fulluserpass = fullhost.substring(0, i); + fullhost = fullhost.substring(i + 1); + + // get user and password + int passindex = fulluserpass.indexOf(':'); + if (passindex != -1) { + username = fulluserpass.substring(0, passindex); + password = fulluserpass.substring(passindex + 1); + } else { + username = fulluserpass; + } + } + + // get the port (if there) + int portindex; + if (fullhost.length() > 0 && fullhost.charAt(0) == '[') { + // an IPv6 address? + portindex = fullhost.indexOf(':', fullhost.indexOf(']')); + } else { + portindex = fullhost.indexOf(':'); + } + if (portindex != -1) { + String portstring = fullhost.substring(portindex + 1); + if (portstring.length() > 0) { + try { + port = Integer.parseInt(portstring); + } catch (NumberFormatException nfex) { + port = -1; + } + } + + host = fullhost.substring(0, portindex); + } else { + host = fullhost; + } + } else { + if (protocolEnd + 1 < len) + file = url.substring(protocolEnd + 1); + } + + // extract the reference from the file name, if any + int refStart; + if (file != null && (refStart = file.indexOf('#')) != -1) { + ref = file.substring(refStart + 1); + file = file.substring(0, refStart); + } + } + + /** + * Returns the port number of this URLName. + * Returns -1 if the port is not set. + * + * @return the port number + */ + public int getPort() { + return port; + } + + /** + * Returns the protocol of this URLName. + * Returns null if this URLName has no protocol. + * + * @return the protocol + */ + public String getProtocol() { + return protocol; + } + + /** + * Returns the file name of this URLName. + * Returns null if this URLName has no file name. + * + * @return the file name of this URLName + */ + public String getFile() { + return file; + } + + /** + * Returns the reference of this URLName. + * Returns null if this URLName has no reference. + * + * @return the reference part of the URLName + */ + public String getRef() { + return ref; + } + + /** + * Returns the host of this URLName. + * Returns null if this URLName has no host. + * + * @return the host name + */ + public String getHost() { + return host; + } + + /** + * Returns the user name of this URLName. + * Returns null if this URLName has no user name. + * + * @return the user name + */ + public String getUsername() { + return doEncode ? decode(username) : username; + } + + /** + * Returns the password of this URLName. + * Returns null if this URLName has no password. + * + * @return the password + */ + public String getPassword() { + return doEncode ? decode(password) : password; + } + + /** + * Constructs a URL from the URLName. + * + * @return the URL + * @exception MalformedURLException if the URL is malformed + */ + public URL getURL() throws MalformedURLException { + // URL expects the file to include the separating "/" + String f = getFile(); + if (f == null) + f = ""; + else + f = "/" + f; + return new URL(getProtocol(), getHost(), getPort(), f); + } + + /** + * Compares two URLNames. The result is true if and only if the + * argument is not null and is a URLName object that represents the + * same URLName as this object. Two URLName objects are equal if + * they have the same protocol and the same host, + * the same port number on the host, the same username, + * and the same file on the host. The fields (host, username, + * file) are also considered the same if they are both + * null.

+ * + * Hosts are considered equal if the names are equal (case independent) + * or if host name lookups for them both succeed and they both reference + * the same IP address.

+ * + * Note that URLName has no knowledge of default port numbers for + * particular protocols, so "imap://host" and "imap://host:143" + * would not compare as equal.

+ * + * Note also that the password field is not included in the comparison, + * nor is any reference field appended to the filename. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof URLName)) + return false; + URLName u2 = (URLName)obj; + + // compare protocols + if (!(protocol == u2.protocol || + (protocol != null && protocol.equals(u2.protocol)))) + return false; + + // compare hosts + InetAddress a1 = getHostAddress(), a2 = u2.getHostAddress(); + // if we have internet address for both, and they're not the same, fail + if (a1 != null && a2 != null) { + if (!a1.equals(a2)) + return false; + // else, if we have host names for both, and they're not the same, fail + } else if (host != null && u2.host != null) { + if (!host.equalsIgnoreCase(u2.host)) + return false; + // else, if not both null + } else if (host != u2.host) { + return false; + } + // at this point, hosts match + + // compare usernames + if (!(username == u2.username || + (username != null && username.equals(u2.username)))) + return false; + + // Forget about password since it doesn't + // really denote a different store. + + // compare files + String f1 = file == null ? "" : file; + String f2 = u2.file == null ? "" : u2.file; + + if (!f1.equals(f2)) + return false; + + // compare ports + if (port != u2.port) + return false; + + // all comparisons succeeded, they're equal + return true; + } + + /** + * Compute the hash code for this URLName. + */ + @Override + public int hashCode() { + if (hashCode != 0) + return hashCode; + if (protocol != null) + hashCode += protocol.hashCode(); + InetAddress addr = getHostAddress(); + if (addr != null) + hashCode += addr.hashCode(); + else if (host != null) + hashCode += host.toLowerCase(Locale.ENGLISH).hashCode(); + if (username != null) + hashCode += username.hashCode(); + if (file != null) + hashCode += file.hashCode(); + hashCode += port; + return hashCode; + } + + /** + * Get the IP address of our host. Look up the + * name the first time and remember that we've done + * so, whether the lookup fails or not. + */ + private synchronized InetAddress getHostAddress() { + if (hostAddressKnown) + return hostAddress; + if (host == null) + return null; + try { + hostAddress = InetAddress.getByName(host); + } catch (UnknownHostException ex) { + hostAddress = null; + } + hostAddressKnown = true; + return hostAddress; + } + + /** + * The class contains a utility method for converting a + * String into a MIME format called + * "x-www-form-urlencoded" format. + *

+ * To convert a String, each character is examined in turn: + *

    + *
  • The ASCII characters 'a' through 'z', + * 'A' through 'Z', '0' + * through '9', and ".", "-", + * "*", "_" remain the same. + *
  • The space character ' ' is converted into a + * plus sign '+'. + *
  • All other characters are converted into the 3-character string + * "%xy", where xy is the two-digit + * hexadecimal representation of the lower 8-bits of the character. + *
+ * + * @author Herb Jellinek + * @since JDK1.0 + */ + static BitSet dontNeedEncoding; + static final int caseDiff = ('a' - 'A'); + + /* The list of characters that are not encoded have been determined by + referencing O'Reilly's "HTML: The Definitive Guide" (page 164). */ + + static { + dontNeedEncoding = new BitSet(256); + int i; + for (i = 'a'; i <= 'z'; i++) { + dontNeedEncoding.set(i); + } + for (i = 'A'; i <= 'Z'; i++) { + dontNeedEncoding.set(i); + } + for (i = '0'; i <= '9'; i++) { + dontNeedEncoding.set(i); + } + /* encoding a space to a + is done in the encode() method */ + dontNeedEncoding.set(' '); + dontNeedEncoding.set('-'); + dontNeedEncoding.set('_'); + dontNeedEncoding.set('.'); + dontNeedEncoding.set('*'); + } + + /** + * Translates a string into x-www-form-urlencoded format. + * + * @param s String to be translated. + * @return the translated String. + */ + static String encode(String s) { + if (s == null) + return null; + // the common case is no encoding is needed + for (int i = 0; i < s.length(); i++) { + int c = (int)s.charAt(i); + if (c == ' ' || !dontNeedEncoding.get(c)) + return _encode(s); + } + return s; + } + + private static String _encode(String s) { + int maxBytesPerChar = 10; + StringBuilder out = new StringBuilder(s.length()); + ByteArrayOutputStream buf = new ByteArrayOutputStream(maxBytesPerChar); + OutputStreamWriter writer = new OutputStreamWriter(buf); + + for (int i = 0; i < s.length(); i++) { + int c = (int)s.charAt(i); + if (dontNeedEncoding.get(c)) { + if (c == ' ') { + c = '+'; + } + out.append((char)c); + } else { + // convert to external encoding before hex conversion + try { + writer.write(c); + writer.flush(); + } catch(IOException e) { + buf.reset(); + continue; + } + byte[] ba = buf.toByteArray(); + for (int j = 0; j < ba.length; j++) { + out.append('%'); + char ch = Character.forDigit((ba[j] >> 4) & 0xF, 16); + // converting to use uppercase letter as part of + // the hex value if ch is a letter. + if (Character.isLetter(ch)) { + ch -= caseDiff; + } + out.append(ch); + ch = Character.forDigit(ba[j] & 0xF, 16); + if (Character.isLetter(ch)) { + ch -= caseDiff; + } + out.append(ch); + } + buf.reset(); + } + } + + return out.toString(); + } + + + /** + * The class contains a utility method for converting from + * a MIME format called "x-www-form-urlencoded" + * to a String + *

+ * To convert to a String, each character is examined in turn: + *

    + *
  • The ASCII characters 'a' through 'z', + * 'A' through 'Z', and '0' + * through '9' remain the same. + *
  • The plus sign '+'is converted into a + * space character ' '. + *
  • The remaining characters are represented by 3-character + * strings which begin with the percent sign, + * "%xy", where xy is the two-digit + * hexadecimal representation of the lower 8-bits of the character. + *
+ * + * @author Mark Chamness + * @author Michael McCloskey + * @since 1.2 + */ + + /** + * Decodes a "x-www-form-urlencoded" + * to a String. + * @param s the String to decode + * @return the newly decoded String + */ + static String decode(String s) { + if (s == null) + return null; + if (indexOfAny(s, "+%") == -1) + return s; // the common case + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '+': + sb.append(' '); + break; + case '%': + try { + sb.append((char)Integer.parseInt( + s.substring(i+1,i+3),16)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "Illegal URL encoded value: " + + s.substring(i,i+3)); + } + i += 2; + break; + default: + sb.append(c); + break; + } + } + // Undo conversion to external encoding + String result = sb.toString(); + try { + byte[] inputBytes = result.getBytes("8859_1"); + result = new String(inputBytes); + } catch (UnsupportedEncodingException e) { + // The system should always have 8859_1 + } + return result; + } + + /** + * Return the first index of any of the characters in "any" in "s", + * or -1 if none are found. + * + * This should be a method on String. + */ + private static int indexOfAny(String s, String any) { + return indexOfAny(s, any, 0); + } + + private static int indexOfAny(String s, String any, int start) { + try { + int len = s.length(); + for (int i = start; i < len; i++) { + if (any.indexOf(s.charAt(i)) >= 0) + return i; + } + return -1; + } catch (StringIndexOutOfBoundsException e) { + return -1; + } + } + + /* + // Do not remove, this is needed when testing new URL cases + public static void main(String[] argv) { + String [] testURLNames = { + "protocol://userid:password@host:119/file", + "http://funny/folder/file.html", + "http://funny/folder/file.html#ref", + "http://funny/folder/file.html#", + "http://funny/#ref", + "imap://jmr:secret@labyrinth//var/mail/jmr", + "nntp://fred@labyrinth:143/save/it/now.mbox", + "imap://jmr@labyrinth/INBOX", + "imap://labryrinth", + "imap://labryrinth/", + "file:", + "file:INBOX", + "file:/home/shannon/mail/foo", + "/tmp/foo", + "//host/tmp/foo", + ":/tmp/foo", + "/really/weird:/tmp/foo#bar", + "" + }; + + URLName url = + new URLName("protocol", "host", 119, "file", "userid", "password"); + System.out.println("Test URL: " + url.toString()); + if (argv.length == 0) { + for (int i = 0; i < testURLNames.length; i++) { + print(testURLNames[i]); + System.out.println(); + } + } else { + for (int i = 0; i < argv.length; i++) { + print(argv[i]); + System.out.println(); + } + if (argv.length == 2) { + URLName u1 = new URLName(argv[0]); + URLName u2 = new URLName(argv[1]); + System.out.println("URL1 hash code: " + u1.hashCode()); + System.out.println("URL2 hash code: " + u2.hashCode()); + if (u1.equals(u2)) + System.out.println("success, equal"); + else + System.out.println("fail, not equal"); + if (u2.equals(u1)) + System.out.println("success, equal"); + else + System.out.println("fail, not equal"); + if (u1.hashCode() == u2.hashCode()) + System.out.println("success, hashCodes equal"); + else + System.out.println("fail, hashCodes not equal"); + } + } + } + + private static void print(String name) { + URLName url = new URLName(name); + System.out.println("Original URL: " + name); + System.out.println("The fullUrl : " + url.toString()); + if (!name.equals(url.toString())) + System.out.println(" : NOT EQUAL!"); + System.out.println("The protocol is: " + url.getProtocol()); + System.out.println("The host is: " + url.getHost()); + System.out.println("The port is: " + url.getPort()); + System.out.println("The user is: " + url.getUsername()); + System.out.println("The password is: " + url.getPassword()); + System.out.println("The file is: " + url.getFile()); + System.out.println("The ref is: " + url.getRef()); + } + */ +} diff --git a/app/src/main/java/javax/mail/Version.java b/app/src/main/java/javax/mail/Version.java new file mode 100644 index 0000000000..9f1793abca --- /dev/null +++ b/app/src/main/java/javax/mail/Version.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail; + +/** + * Package-private class that defines the version of Jakarta Mail. + * This file is a template for the class that is generated + * at build time. + */ +class Version { + public static final String version = "1.6.5-SNAPSHOT"; +} diff --git a/app/src/main/java/javax/mail/event/ConnectionAdapter.java b/app/src/main/java/javax/mail/event/ConnectionAdapter.java new file mode 100644 index 0000000000..a07e1511ef --- /dev/null +++ b/app/src/main/java/javax/mail/event/ConnectionAdapter.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +/** + * The adapter which receives connection events. + * The methods in this class are empty; this class is provided as a + * convenience for easily creating listeners by extending this class + * and overriding only the methods of interest. + * + * @author John Mani + */ +public abstract class ConnectionAdapter implements ConnectionListener { + @Override + public void opened(ConnectionEvent e) {} + @Override + public void disconnected(ConnectionEvent e) {} + @Override + public void closed(ConnectionEvent e) {} +} diff --git a/app/src/main/java/javax/mail/event/ConnectionEvent.java b/app/src/main/java/javax/mail/event/ConnectionEvent.java new file mode 100644 index 0000000000..c89a003ba7 --- /dev/null +++ b/app/src/main/java/javax/mail/event/ConnectionEvent.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; +import javax.mail.*; + +/** + * This class models Connection events. + * + * @author John Mani + */ + +public class ConnectionEvent extends MailEvent { + + /** A connection was opened. */ + public static final int OPENED = 1; + /** A connection was disconnected (not currently used). */ + public static final int DISCONNECTED = 2; + /** A connection was closed. */ + public static final int CLOSED = 3; + + /** + * The event type. + * + * @serial + */ + protected int type; + + private static final long serialVersionUID = -1855480171284792957L; + + /** + * Construct a ConnectionEvent. + * + * @param source The source object + * @param type the event type + */ + public ConnectionEvent(Object source, int type) { + super(source); + this.type = type; + } + + /** + * Return the type of this event + * @return type + */ + public int getType() { + return type; + } + + /** + * Invokes the appropriate ConnectionListener method + */ + @Override + public void dispatch(Object listener) { + if (type == OPENED) + ((ConnectionListener)listener).opened(this); + else if (type == DISCONNECTED) + ((ConnectionListener)listener).disconnected(this); + else if (type == CLOSED) + ((ConnectionListener)listener).closed(this); + } +} diff --git a/app/src/main/java/javax/mail/event/ConnectionListener.java b/app/src/main/java/javax/mail/event/ConnectionListener.java new file mode 100644 index 0000000000..64721dfd71 --- /dev/null +++ b/app/src/main/java/javax/mail/event/ConnectionListener.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; + +/** + * This is the Listener interface for Connection events. + * + * @author John Mani + */ + +public interface ConnectionListener extends java.util.EventListener { + + /** + * Invoked when a Store/Folder/Transport is opened. + * + * @param e the ConnectionEvent + */ + public void opened(ConnectionEvent e); + + /** + * Invoked when a Store is disconnected. Note that a folder + * cannot be disconnected, so a folder will not fire this event + * + * @param e the ConnectionEvent + */ + public void disconnected(ConnectionEvent e); + + /** + * Invoked when a Store/Folder/Transport is closed. + * + * @param e the ConnectionEvent + */ + public void closed(ConnectionEvent e); +} diff --git a/app/src/main/java/javax/mail/event/FolderAdapter.java b/app/src/main/java/javax/mail/event/FolderAdapter.java new file mode 100644 index 0000000000..7bdd009f92 --- /dev/null +++ b/app/src/main/java/javax/mail/event/FolderAdapter.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +/** + * The adapter which receives Folder events. + * The methods in this class are empty; this class is provided as a + * convenience for easily creating listeners by extending this class + * and overriding only the methods of interest. + * + * @author John Mani + */ +public abstract class FolderAdapter implements FolderListener { + @Override + public void folderCreated(FolderEvent e) {} + @Override + public void folderRenamed(FolderEvent e) {} + @Override + public void folderDeleted(FolderEvent e) {} +} diff --git a/app/src/main/java/javax/mail/event/FolderEvent.java b/app/src/main/java/javax/mail/event/FolderEvent.java new file mode 100644 index 0000000000..4c89ae63ab --- /dev/null +++ b/app/src/main/java/javax/mail/event/FolderEvent.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; +import javax.mail.*; + +/** + * This class models Folder existence events. FolderEvents are + * delivered to FolderListeners registered on the affected Folder as + * well as the containing Store.

+ * + * Service providers vary widely in their ability to notify clients of + * these events. At a minimum, service providers must notify listeners + * registered on the same Store or Folder object on which the operation + * occurs. Service providers may also notify listeners when changes + * are made through operations on other objects in the same virtual + * machine, or by other clients in the same or other hosts. Such + * notifications are not required and are typically not supported + * by mail protocols (including IMAP). + * + * @author John Mani + * @author Bill Shannon + */ + +public class FolderEvent extends MailEvent { + + /** The folder was created. */ + public static final int CREATED = 1; + /** The folder was deleted. */ + public static final int DELETED = 2; + /** The folder was renamed. */ + public static final int RENAMED = 3; + + /** + * The event type. + * + * @serial + */ + protected int type; + + /** + * The folder the event occurred on. + */ + transient protected Folder folder; + + /** + * The folder that represents the new name, in case of a RENAMED event. + * + * @since JavaMail 1.1 + */ + transient protected Folder newFolder; + + private static final long serialVersionUID = 5278131310563694307L; + + /** + * Constructor.

+ * + * @param source The source of the event + * @param folder The affected folder + * @param type The event type + */ + public FolderEvent(Object source, Folder folder, int type) { + this(source, folder, folder, type); + } + + /** + * Constructor. Use for RENAMED events. + * + * @param source The source of the event + * @param oldFolder The folder that is renamed + * @param newFolder The folder that represents the new name + * @param type The event type + * @since JavaMail 1.1 + */ + public FolderEvent(Object source, Folder oldFolder, + Folder newFolder, int type) { + super(source); + this.folder = oldFolder; + this.newFolder = newFolder; + this.type = type; + } + + /** + * Return the type of this event. + * + * @return type + */ + public int getType() { + return type; + } + + /** + * Return the affected folder. + * + * @return the affected folder + * @see #getNewFolder + */ + public Folder getFolder() { + return folder; + } + + /** + * If this event indicates that a folder is renamed, (i.e, the event type + * is RENAMED), then this method returns the Folder object representing the + * new name.

+ * + * The getFolder() method returns the folder that is renamed. + * + * @return Folder representing the new name. + * @see #getFolder + * @since JavaMail 1.1 + */ + public Folder getNewFolder() { + return newFolder; + } + + /** + * Invokes the appropriate FolderListener method + */ + @Override + public void dispatch(Object listener) { + if (type == CREATED) + ((FolderListener)listener).folderCreated(this); + else if (type == DELETED) + ((FolderListener)listener).folderDeleted(this); + else if (type == RENAMED) + ((FolderListener)listener).folderRenamed(this); + } +} diff --git a/app/src/main/java/javax/mail/event/FolderListener.java b/app/src/main/java/javax/mail/event/FolderListener.java new file mode 100644 index 0000000000..5c652a35c3 --- /dev/null +++ b/app/src/main/java/javax/mail/event/FolderListener.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; + +/** + * This is the Listener interface for Folder events. + * + * @author John Mani + */ + +public interface FolderListener extends java.util.EventListener { + /** + * Invoked when a Folder is created. + * + * @param e the FolderEvent + */ + public void folderCreated(FolderEvent e); + + /** + * Invoked when a folder is deleted. + * + * @param e the FolderEvent + */ + public void folderDeleted(FolderEvent e); + + /** + * Invoked when a folder is renamed. + * + * @param e the FolderEvent + */ + public void folderRenamed(FolderEvent e); +} diff --git a/app/src/main/java/javax/mail/event/MailEvent.java b/app/src/main/java/javax/mail/event/MailEvent.java new file mode 100644 index 0000000000..97e419160e --- /dev/null +++ b/app/src/main/java/javax/mail/event/MailEvent.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.EventObject; + +/** + * Common base class for mail events, defining the dispatch method. + * + * @author Bill Shannon + */ + +public abstract class MailEvent extends EventObject { + private static final long serialVersionUID = 1846275636325456631L; + + /** + * Construct a MailEvent referring to the given source. + * + * @param source the source of the event + */ + public MailEvent(Object source) { + super(source); + } + + /** + * This method invokes the appropriate method on a listener for + * this event. Subclasses provide the implementation. + * + * @param listener the listener to invoke on + */ + public abstract void dispatch(Object listener); +} diff --git a/app/src/main/java/javax/mail/event/MessageChangedEvent.java b/app/src/main/java/javax/mail/event/MessageChangedEvent.java new file mode 100644 index 0000000000..0099592aa9 --- /dev/null +++ b/app/src/main/java/javax/mail/event/MessageChangedEvent.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; +import javax.mail.*; + +/** + * This class models Message change events. + * + * @author John Mani + */ + +public class MessageChangedEvent extends MailEvent { + + /** The message's flags changed. */ + public static final int FLAGS_CHANGED = 1; + /** The message's envelope (headers, but not body) changed. */ + public static final int ENVELOPE_CHANGED = 2; + + /** + * The event type. + * + * @serial + */ + protected int type; + + /** + * The message that changed. + */ + transient protected Message msg; + + private static final long serialVersionUID = -4974972972105535108L; + + /** + * Constructor. + * @param source The folder that owns the message + * @param type The change type + * @param msg The changed message + */ + public MessageChangedEvent(Object source, int type, Message msg) { + super(source); + this.msg = msg; + this.type = type; + } + + /** + * Return the type of this event. + * @return type + */ + public int getMessageChangeType() { + return type; + } + + /** + * Return the changed Message. + * @return the message + */ + public Message getMessage() { + return msg; + } + + /** + * Invokes the appropriate MessageChangedListener method. + */ + @Override + public void dispatch(Object listener) { + ((MessageChangedListener)listener).messageChanged(this); + } +} diff --git a/app/src/main/java/javax/mail/event/MessageChangedListener.java b/app/src/main/java/javax/mail/event/MessageChangedListener.java new file mode 100644 index 0000000000..c4eac48e37 --- /dev/null +++ b/app/src/main/java/javax/mail/event/MessageChangedListener.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; + +/** + * This is the Listener interface for MessageChanged events + * + * @author John Mani + */ + +public interface MessageChangedListener extends java.util.EventListener { + /** + * Invoked when a message is changed. The change-type specifies + * what changed. + * + * @param e the MessageChangedEvent + * @see MessageChangedEvent#FLAGS_CHANGED + * @see MessageChangedEvent#ENVELOPE_CHANGED + */ + public void messageChanged(MessageChangedEvent e); +} diff --git a/app/src/main/java/javax/mail/event/MessageCountAdapter.java b/app/src/main/java/javax/mail/event/MessageCountAdapter.java new file mode 100644 index 0000000000..694069fd8c --- /dev/null +++ b/app/src/main/java/javax/mail/event/MessageCountAdapter.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +/** + * The adapter which receives MessageCount events. + * The methods in this class are empty; this class is provided as a + * convenience for easily creating listeners by extending this class + * and overriding only the methods of interest. + * + * @author John Mani + */ +public abstract class MessageCountAdapter implements MessageCountListener { + @Override + public void messagesAdded(MessageCountEvent e) {} + @Override + public void messagesRemoved(MessageCountEvent e) {} +} diff --git a/app/src/main/java/javax/mail/event/MessageCountEvent.java b/app/src/main/java/javax/mail/event/MessageCountEvent.java new file mode 100644 index 0000000000..24d38a845d --- /dev/null +++ b/app/src/main/java/javax/mail/event/MessageCountEvent.java @@ -0,0 +1,134 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; +import javax.mail.*; + +/** + * This class notifies changes in the number of messages in a folder.

+ * + * Note that some folder types may only deliver MessageCountEvents at + * certain times or after certain operations. IMAP in particular will + * only notify the client of MessageCountEvents when a client issues a + * new command. Refer to + * RFC 3501 + * for details. + * A client may want to "poll" the folder by occasionally calling the + * {@link javax.mail.Folder#getMessageCount getMessageCount} or + * {@link javax.mail.Folder#isOpen isOpen} methods + * to solicit any such notifications. + * + * @author John Mani + */ + +public class MessageCountEvent extends MailEvent { + + /** The messages were added to their folder */ + public static final int ADDED = 1; + /** The messages were removed from their folder */ + public static final int REMOVED = 2; + + /** + * The event type. + * + * @serial + */ + protected int type; + + /** + * If true, this event is the result of an explicit + * expunge by this client, and the messages in this + * folder have been renumbered to account for this. + * If false, this event is the result of an expunge + * by external sources. + * + * @serial + */ + protected boolean removed; + + /** + * The messages. + */ + transient protected Message[] msgs; + + private static final long serialVersionUID = -7447022340837897369L; + + /** + * Constructor. + * @param folder The containing folder + * @param type The event type + * @param removed If true, this event is the result of an explicit + * expunge by this client, and the messages in this + * folder have been renumbered to account for this. + * If false, this event is the result of an expunge + * by external sources. + * + * @param msgs The messages added/removed + */ + public MessageCountEvent(Folder folder, int type, + boolean removed, Message[] msgs) { + super(folder); + this.type = type; + this.removed = removed; + this.msgs = msgs; + } + + /** + * Return the type of this event. + * @return type + */ + public int getType() { + return type; + } + + /** + * Indicates whether this event is the result of an explicit + * expunge by this client, or due to an expunge from external + * sources. If true, this event is due to an + * explicit expunge and hence all remaining messages in this + * folder have been renumbered. If false, this event + * is due to an external expunge.

+ * + * Note that this method is valid only if the type of this event + * is REMOVED + * + * @return true if the message has been removed + */ + public boolean isRemoved() { + return removed; + } + + /** + * Return the array of messages added or removed. + * @return array of messages + */ + public Message[] getMessages() { + return msgs; + } + + /** + * Invokes the appropriate MessageCountListener method. + */ + @Override + public void dispatch(Object listener) { + if (type == ADDED) + ((MessageCountListener)listener).messagesAdded(this); + else // REMOVED + ((MessageCountListener)listener).messagesRemoved(this); + } +} diff --git a/app/src/main/java/javax/mail/event/MessageCountListener.java b/app/src/main/java/javax/mail/event/MessageCountListener.java new file mode 100644 index 0000000000..5793fe04bb --- /dev/null +++ b/app/src/main/java/javax/mail/event/MessageCountListener.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; + +/** + * This is the Listener interface for MessageCount events. + * + * @author John Mani + */ + +public interface MessageCountListener extends java.util.EventListener { + /** + * Invoked when messages are added into a folder. + * + * @param e the MessageCountEvent + */ + public void messagesAdded(MessageCountEvent e); + + /** + * Invoked when messages are removed (expunged) from a folder. + * + * @param e the MessageCountEvent + */ + public void messagesRemoved(MessageCountEvent e); +} diff --git a/app/src/main/java/javax/mail/event/StoreEvent.java b/app/src/main/java/javax/mail/event/StoreEvent.java new file mode 100644 index 0000000000..b4332836fd --- /dev/null +++ b/app/src/main/java/javax/mail/event/StoreEvent.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; +import javax.mail.*; + +/** + * This class models notifications from the Store connection. These + * notifications can be ALERTS or NOTICES. ALERTS must be presented + * to the user in a fashion that calls the user's attention to the + * message. + * + * @author John Mani + */ + +public class StoreEvent extends MailEvent { + + /** + * Indicates that this message is an ALERT. + */ + public static final int ALERT = 1; + + /** + * Indicates that this message is a NOTICE. + */ + public static final int NOTICE = 2; + + /** + * The event type. + * + * @serial + */ + protected int type; + + /** + * The message text to be presented to the user. + * + * @serial + */ + protected String message; + + private static final long serialVersionUID = 1938704919992515330L; + + /** + * Construct a StoreEvent. + * + * @param store the source Store + * @param type the event type + * @param message a message assoicated with the event + */ + public StoreEvent(Store store, int type, String message) { + super(store); + this.type = type; + this.message = message; + } + + /** + * Return the type of this event. + * + * @return type + * @see #ALERT + * @see #NOTICE + */ + public int getMessageType() { + return type; + } + + /** + * Get the message from the Store. + * + * @return message from the Store + */ + public String getMessage() { + return message; + } + + /** + * Invokes the appropriate StoreListener method. + */ + @Override + public void dispatch(Object listener) { + ((StoreListener)listener).notification(this); + } +} diff --git a/app/src/main/java/javax/mail/event/StoreListener.java b/app/src/main/java/javax/mail/event/StoreListener.java new file mode 100644 index 0000000000..3c00ab8280 --- /dev/null +++ b/app/src/main/java/javax/mail/event/StoreListener.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; + +/** + * This is the Listener interface for Store Notifications. + * + * @author John Mani + */ + +public interface StoreListener extends java.util.EventListener { + + /** + * Invoked when the Store generates a notification event. + * + * @param e the StoreEvent + * @see StoreEvent#ALERT + * @see StoreEvent#NOTICE + */ + public void notification(StoreEvent e); +} diff --git a/app/src/main/java/javax/mail/event/TransportAdapter.java b/app/src/main/java/javax/mail/event/TransportAdapter.java new file mode 100644 index 0000000000..798cef7cb0 --- /dev/null +++ b/app/src/main/java/javax/mail/event/TransportAdapter.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +/** + * The adapter which receives Transport events. + * The methods in this class are empty; this class is provided as a + * convenience for easily creating listeners by extending this class + * and overriding only the methods of interest. + * + * @author John Mani + */ +public abstract class TransportAdapter implements TransportListener { + @Override + public void messageDelivered(TransportEvent e) {} + @Override + public void messageNotDelivered(TransportEvent e) {} + @Override + public void messagePartiallyDelivered(TransportEvent e) {} +} diff --git a/app/src/main/java/javax/mail/event/TransportEvent.java b/app/src/main/java/javax/mail/event/TransportEvent.java new file mode 100644 index 0000000000..2f754ca263 --- /dev/null +++ b/app/src/main/java/javax/mail/event/TransportEvent.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; +import javax.mail.*; + +/** + * This class models Transport events. + * + * @author John Mani + * @author Max Spivak + * + * @see javax.mail.Transport + * @see javax.mail.event.TransportListener + */ + +public class TransportEvent extends MailEvent { + + /** + * Message has been successfully delivered to all recipients by the + * transport firing this event. validSent[] contains all the addresses + * this transport sent to successfully. validUnsent[] and invalid[] + * should be null, + */ + public static final int MESSAGE_DELIVERED = 1; + + /** + * Message was not sent for some reason. validSent[] should be null. + * validUnsent[] may have addresses that are valid (but the message + * wasn't sent to them). invalid[] should likely contain invalid addresses. + */ + public static final int MESSAGE_NOT_DELIVERED = 2; + + /** + * Message was successfully sent to some recipients but not to all. + * validSent[] holds addresses of recipients to whom the message was sent. + * validUnsent[] holds valid addresses to which the message was not sent. + * invalid[] holds invalid addresses, if any. + */ + public static final int MESSAGE_PARTIALLY_DELIVERED = 3; + + + /** + * The event type. + * + * @serial + */ + protected int type; + + /** The valid address to which the message was sent. */ + transient protected Address[] validSent; + /** The valid address to which the message was not sent. */ + transient protected Address[] validUnsent; + /** The invalid addresses. */ + transient protected Address[] invalid; + /** The Message to which this event applies. */ + transient protected Message msg; + + private static final long serialVersionUID = -4729852364684273073L; + + /** + * Constructor. + * + * @param transport The Transport object + * @param type the event type (MESSAGE_DELIVERED, etc.) + * @param validSent the valid addresses to which the message was sent + * @param validUnsent the valid addresses to which the message was + * not sent + * @param invalid the invalid addresses + * @param msg the message being sent + */ + public TransportEvent(Transport transport, int type, Address[] validSent, + Address[] validUnsent, Address[] invalid, + Message msg) { + super(transport); + this.type = type; + this.validSent = validSent; + this.validUnsent = validUnsent; + this.invalid = invalid; + this.msg = msg; + } + + /** + * Return the type of this event. + * @return type + */ + public int getType() { + return type; + } + + /** + * Return the addresses to which this message was sent succesfully. + * @return Addresses to which the message was sent successfully or null + */ + public Address[] getValidSentAddresses() { + return validSent; + } + + /** + * Return the addresses that are valid but to which this message + * was not sent. + * @return Addresses that are valid but to which the message was + * not sent successfully or null + */ + public Address[] getValidUnsentAddresses() { + return validUnsent; + } + + /** + * Return the addresses to which this message could not be sent. + * @return Addresses to which the message sending failed or null + */ + public Address[] getInvalidAddresses() { + return invalid; + } + + /** + * Get the Message object associated with this Transport Event. + * + * @return the Message object + * @since JavaMail 1.2 + */ + public Message getMessage() { + return msg; + } + + /** + * Invokes the appropriate TransportListener method. + */ + @Override + public void dispatch(Object listener) { + if (type == MESSAGE_DELIVERED) + ((TransportListener)listener).messageDelivered(this); + else if (type == MESSAGE_NOT_DELIVERED) + ((TransportListener)listener).messageNotDelivered(this); + else // MESSAGE_PARTIALLY_DELIVERED + ((TransportListener)listener).messagePartiallyDelivered(this); + } +} diff --git a/app/src/main/java/javax/mail/event/TransportListener.java b/app/src/main/java/javax/mail/event/TransportListener.java new file mode 100644 index 0000000000..3fc030d570 --- /dev/null +++ b/app/src/main/java/javax/mail/event/TransportListener.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.event; + +import java.util.*; + +/** + * This is the Listener interface for Transport events + * + * @author John Mani + * @author Max Spivak + * + * @see javax.mail.Transport + * @see javax.mail.event.TransportEvent + */ + +public interface TransportListener extends java.util.EventListener { + + /** + * Invoked when a Message is succesfully delivered. + * @param e TransportEvent + */ + public void messageDelivered(TransportEvent e); + + /** + * Invoked when a Message is not delivered. + * @param e TransportEvent + * @see TransportEvent + */ + public void messageNotDelivered(TransportEvent e); + + /** + * Invoked when a Message is partially delivered. + * @param e TransportEvent + * @see TransportEvent + */ + public void messagePartiallyDelivered(TransportEvent e); +} diff --git a/app/src/main/java/javax/mail/event/package.html b/app/src/main/java/javax/mail/event/package.html new file mode 100644 index 0000000000..04889ff6ca --- /dev/null +++ b/app/src/main/java/javax/mail/event/package.html @@ -0,0 +1,31 @@ + + + + + + + + + +Listeners and events for the Jakarta Mail API. +This package defines listener classes and event classes used by the classes +defined in the javax.mail package. + + + diff --git a/app/src/main/java/javax/mail/internet/AddressException.java b/app/src/main/java/javax/mail/internet/AddressException.java new file mode 100644 index 0000000000..b91e1ceda9 --- /dev/null +++ b/app/src/main/java/javax/mail/internet/AddressException.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +/** + * The exception thrown when a wrongly formatted address is encountered. + * + * @author Bill Shannon + * @author Max Spivak + */ + +public class AddressException extends ParseException { + /** + * The string being parsed. + * + * @serial + */ + protected String ref = null; + + /** + * The index in the string where the error occurred, or -1 if not known. + * + * @serial + */ + protected int pos = -1; + + private static final long serialVersionUID = 9134583443539323120L; + + /** + * Constructs an AddressException with no detail message. + */ + public AddressException() { + super(); + } + + /** + * Constructs an AddressException with the specified detail message. + * @param s the detail message + */ + public AddressException(String s) { + super(s); + } + + /** + * Constructs an AddressException with the specified detail message + * and reference info. + * + * @param s the detail message + * @param ref the string being parsed + */ + public AddressException(String s, String ref) { + super(s); + this.ref = ref; + } + + /** + * Constructs an AddressException with the specified detail message + * and reference info. + * + * @param s the detail message + * @param ref the string being parsed + * @param pos the position of the error + */ + public AddressException(String s, String ref, int pos) { + super(s); + this.ref = ref; + this.pos = pos; + } + + /** + * Get the string that was being parsed when the error was detected + * (null if not relevant). + * + * @return the string that was being parsed + */ + public String getRef() { + return ref; + } + + /** + * Get the position with the reference string where the error was + * detected (-1 if not relevant). + * + * @return the position within the string of the error + */ + public int getPos() { + return pos; + } + + @Override + public String toString() { + String s = super.toString(); + if (ref == null) + return s; + s += " in string ``" + ref + "''"; + if (pos < 0) + return s; + return s + " at position " + pos; + } +} diff --git a/app/src/main/java/javax/mail/internet/ContentDisposition.java b/app/src/main/java/javax/mail/internet/ContentDisposition.java new file mode 100644 index 0000000000..123a1ef42e --- /dev/null +++ b/app/src/main/java/javax/mail/internet/ContentDisposition.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import javax.mail.*; +import java.util.*; +import java.io.*; +import com.sun.mail.util.PropUtil; + +/** + * This class represents a MIME ContentDisposition value. It provides + * methods to parse a ContentDisposition string into individual components + * and to generate a MIME style ContentDisposition string. + * + * @author John Mani + */ + +public class ContentDisposition { + + private static final boolean contentDispositionStrict = + PropUtil.getBooleanSystemProperty("mail.mime.contentdisposition.strict", true); + + private String disposition; // disposition + private ParameterList list; // parameter list + + /** + * No-arg Constructor. + */ + public ContentDisposition() { } + + /** + * Constructor. + * + * @param disposition disposition + * @param list ParameterList + * @since JavaMail 1.2 + */ + public ContentDisposition(String disposition, ParameterList list) { + this.disposition = disposition; + this.list = list; + } + + /** + * Constructor that takes a ContentDisposition string. The String + * is parsed into its constituents: dispostion and parameters. + * A ParseException is thrown if the parse fails. + * + * @param s the ContentDisposition string. + * @exception ParseException if the parse fails. + * @since JavaMail 1.2 + */ + public ContentDisposition(String s) throws ParseException { + HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME); + HeaderTokenizer.Token tk; + + // First "disposition" .. + tk = h.next(); + if (tk.getType() != HeaderTokenizer.Token.ATOM) { + if (contentDispositionStrict) { + throw new ParseException("Expected disposition, got " + + tk.getValue()); + } + } else { + disposition = tk.getValue(); + } + + // Then parameters .. + String rem = h.getRemainder(); + if (rem != null) { + try { + list = new ParameterList(rem); + } catch (ParseException px) { + if (contentDispositionStrict) { + throw px; + } + } + } + } + + /** + * Return the disposition value. + * @return the disposition + * @since JavaMail 1.2 + */ + public String getDisposition() { + return disposition; + } + + /** + * Return the specified parameter value. Returns null + * if this parameter is absent. + * + * @param name the parameter name + * @return parameter value + * @since JavaMail 1.2 + */ + public String getParameter(String name) { + if (list == null) + return null; + + return list.get(name); + } + + /** + * Return a ParameterList object that holds all the available + * parameters. Returns null if no parameters are available. + * + * @return ParameterList + * @since JavaMail 1.2 + */ + public ParameterList getParameterList() { + return list; + } + + /** + * Set the disposition. Replaces the existing disposition. + * @param disposition the disposition + * @since JavaMail 1.2 + */ + public void setDisposition(String disposition) { + this.disposition = disposition; + } + + /** + * Set the specified parameter. If this parameter already exists, + * it is replaced by this new value. + * + * @param name parameter name + * @param value parameter value + * @since JavaMail 1.2 + */ + public void setParameter(String name, String value) { + if (list == null) + list = new ParameterList(); + + list.set(name, value); + } + + /** + * Set a new ParameterList. + * @param list ParameterList + * @since JavaMail 1.2 + */ + public void setParameterList(ParameterList list) { + this.list = list; + } + + /** + * Retrieve a RFC2045 style string representation of + * this ContentDisposition. Returns an empty string if + * the conversion failed. + * + * @return RFC2045 style string + * @since JavaMail 1.2 + */ + @Override + public String toString() { + if (disposition == null) + return ""; + + if (list == null) + return disposition; + + StringBuilder sb = new StringBuilder(disposition); + + // append the parameter list + // use the length of the string buffer + the length of + // the header name formatted as follows "Content-Disposition: " + sb.append(list.toString(sb.length() + 21)); + return sb.toString(); + } +} diff --git a/app/src/main/java/javax/mail/internet/ContentType.java b/app/src/main/java/javax/mail/internet/ContentType.java new file mode 100644 index 0000000000..2005d2fd66 --- /dev/null +++ b/app/src/main/java/javax/mail/internet/ContentType.java @@ -0,0 +1,274 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import javax.mail.*; +import java.util.*; +import java.io.*; + +/** + * This class represents a MIME Content-Type value. It provides + * methods to parse a Content-Type string into individual components + * and to generate a MIME style Content-Type string. + * + * @author John Mani + */ + +public class ContentType { + + private String primaryType; // primary type + private String subType; // subtype + private ParameterList list; // parameter list + + /** + * No-arg Constructor. + */ + public ContentType() { } + + /** + * Constructor. + * + * @param primaryType primary type + * @param subType subType + * @param list ParameterList + */ + public ContentType(String primaryType, String subType, + ParameterList list) { + this.primaryType = primaryType; + this.subType = subType; + this.list = list; + } + + /** + * Constructor that takes a Content-Type string. The String + * is parsed into its constituents: primaryType, subType + * and parameters. A ParseException is thrown if the parse fails. + * + * @param s the Content-Type string. + * @exception ParseException if the parse fails. + */ + public ContentType(String s) throws ParseException { + HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME); + HeaderTokenizer.Token tk; + + // First "type" .. + tk = h.next(); + if (tk.getType() != HeaderTokenizer.Token.ATOM) + throw new ParseException("In Content-Type string <" + s + ">" + + ", expected MIME type, got " + + tk.getValue()); + primaryType = tk.getValue(); + + // The '/' separator .. + tk = h.next(); + if ((char)tk.getType() != '/') + throw new ParseException("In Content-Type string <" + s + ">" + + ", expected '/', got " + tk.getValue()); + + // Then "subType" .. + tk = h.next(); + if (tk.getType() != HeaderTokenizer.Token.ATOM) + throw new ParseException("In Content-Type string <" + s + ">" + + ", expected MIME subtype, got " + + tk.getValue()); + subType = tk.getValue(); + + // Finally parameters .. + String rem = h.getRemainder(); + if (rem != null) + list = new ParameterList(rem); + } + + /** + * Return the primary type. + * @return the primary type + */ + public String getPrimaryType() { + return primaryType; + } + + /** + * Return the subType. + * @return the subType + */ + public String getSubType() { + return subType; + } + + /** + * Return the MIME type string, without the parameters. + * The returned value is basically the concatenation of + * the primaryType, the '/' character and the secondaryType. + * + * @return the type + */ + public String getBaseType() { + if (primaryType == null || subType == null) + return ""; + return primaryType + '/' + subType; + } + + /** + * Return the specified parameter value. Returns null + * if this parameter is absent. + * + * @param name the parameter name + * @return parameter value + */ + public String getParameter(String name) { + if (list == null) + return null; + + return list.get(name); + } + + /** + * Return a ParameterList object that holds all the available + * parameters. Returns null if no parameters are available. + * + * @return ParameterList + */ + public ParameterList getParameterList() { + return list; + } + + /** + * Set the primary type. Overrides existing primary type. + * @param primaryType primary type + */ + public void setPrimaryType(String primaryType) { + this.primaryType = primaryType; + } + + /** + * Set the subType. Replaces the existing subType. + * @param subType the subType + */ + public void setSubType(String subType) { + this.subType = subType; + } + + /** + * Set the specified parameter. If this parameter already exists, + * it is replaced by this new value. + * + * @param name parameter name + * @param value parameter value + */ + public void setParameter(String name, String value) { + if (list == null) + list = new ParameterList(); + + list.set(name, value); + } + + /** + * Set a new ParameterList. + * @param list ParameterList + */ + public void setParameterList(ParameterList list) { + this.list = list; + } + + /** + * Retrieve a RFC2045 style string representation of + * this Content-Type. Returns an empty string if + * the conversion failed. + * + * @return RFC2045 style string + */ + @Override + public String toString() { + if (primaryType == null || subType == null) // need both + return ""; + + StringBuilder sb = new StringBuilder(); + sb.append(primaryType).append('/').append(subType); + if (list != null) + // append the parameter list + // use the length of the string buffer + the length of + // the header name formatted as follows "Content-Type: " + sb.append(list.toString(sb.length() + 14)); + + return sb.toString(); + } + + /** + * Match with the specified ContentType object. This method + * compares only the primaryType and + * subType . The parameters of both operands + * are ignored.

+ * + * For example, this method will return true when + * comparing the ContentTypes for "text/plain" + * and "text/plain; charset=foobar". + * + * If the subType of either operand is the special + * character '*', then the subtype is ignored during the match. + * For example, this method will return true when + * comparing the ContentTypes for "text/plain" + * and "text/*" + * + * @param cType ContentType to compare this against + * @return true if it matches + */ + public boolean match(ContentType cType) { + // Match primaryType + if (!((primaryType == null && cType.getPrimaryType() == null) || + (primaryType != null && + primaryType.equalsIgnoreCase(cType.getPrimaryType())))) + return false; + + String sType = cType.getSubType(); + + // If either one of the subTypes is wildcarded, return true + if ((subType != null && subType.startsWith("*")) || + (sType != null && sType.startsWith("*"))) + return true; + + // Match subType + return (subType == null && sType == null) || + (subType != null && subType.equalsIgnoreCase(sType)); + } + + /** + * Match with the specified content-type string. This method + * compares only the primaryType and + * subType . + * The parameters of both operands are ignored.

+ * + * For example, this method will return true when + * comparing the ContentType for "text/plain" + * with "text/plain; charset=foobar". + * + * If the subType of either operand is the special + * character '*', then the subtype is ignored during the match. + * For example, this method will return true when + * comparing the ContentType for "text/plain" + * with "text/*" + * + * @param s the content-type string to match + * @return true if it matches + */ + public boolean match(String s) { + try { + return match(new ContentType(s)); + } catch (ParseException pex) { + return false; + } + } +} diff --git a/app/src/main/java/javax/mail/internet/HeaderTokenizer.java b/app/src/main/java/javax/mail/internet/HeaderTokenizer.java new file mode 100644 index 0000000000..e5bafe3cf9 --- /dev/null +++ b/app/src/main/java/javax/mail/internet/HeaderTokenizer.java @@ -0,0 +1,471 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.util.*; + +/** + * This class tokenizes RFC822 and MIME headers into the basic + * symbols specified by RFC822 and MIME.

+ * + * This class handles folded headers (ie headers with embedded + * CRLF SPACE sequences). The folds are removed in the returned + * tokens. + * + * @author John Mani + * @author Bill Shannon + */ + +public class HeaderTokenizer { + + /** + * The Token class represents tokens returned by the + * HeaderTokenizer. + */ + public static class Token { + + private int type; + private String value; + + /** + * Token type indicating an ATOM. + */ + public static final int ATOM = -1; + + /** + * Token type indicating a quoted string. The value + * field contains the string without the quotes. + */ + public static final int QUOTEDSTRING = -2; + + /** + * Token type indicating a comment. The value field + * contains the comment string without the comment + * start and end symbols. + */ + public static final int COMMENT = -3; + + /** + * Token type indicating end of input. + */ + public static final int EOF = -4; + + /** + * Constructor. + * @param type Token type + * @param value Token value + */ + public Token(int type, String value) { + this.type = type; + this.value = value; + } + + /** + * Return the type of the token. If the token represents a + * delimiter or a control character, the type is that character + * itself, converted to an integer. Otherwise, it's value is + * one of the following: + *

    + *
  • ATOM A sequence of ASCII characters + * delimited by either SPACE, CTL, "(", <"> or the + * specified SPECIALS + *
  • QUOTEDSTRING A sequence of ASCII characters + * within quotes + *
  • COMMENT A sequence of ASCII characters + * within "(" and ")". + *
  • EOF End of header + *
+ * + * @return the token type + */ + public int getType() { + return type; + } + + /** + * Returns the value of the token just read. When the current + * token is a quoted string, this field contains the body of the + * string, without the quotes. When the current token is a comment, + * this field contains the body of the comment. + * + * @return token value + */ + public String getValue() { + return value; + } + } + + private String string; // the string to be tokenized + private boolean skipComments; // should comments be skipped ? + private String delimiters; // delimiter string + private int currentPos; // current parse position + private int maxPos; // string length + private int nextPos; // track start of next Token for next() + private int peekPos; // track start of next Token for peek() + + /** + * RFC822 specials + */ + public final static String RFC822 = "()<>@,;:\\\"\t .[]"; + + /** + * MIME specials + */ + public final static String MIME = "()<>@,;:\\\"\t []/?="; + + // The EOF Token + private final static Token EOFToken = new Token(Token.EOF, null); + + /** + * Constructor that takes a rfc822 style header. + * + * @param header The rfc822 header to be tokenized + * @param delimiters Set of delimiter characters + * to be used to delimit ATOMS. These + * are usually RFC822 or + * MIME + * @param skipComments If true, comments are skipped and + * not returned as tokens + */ + public HeaderTokenizer(String header, String delimiters, + boolean skipComments) { + string = (header == null) ? "" : header; // paranoia ?! + this.skipComments = skipComments; + this.delimiters = delimiters; + currentPos = nextPos = peekPos = 0; + maxPos = string.length(); + } + + /** + * Constructor. Comments are ignored and not returned as tokens + * + * @param header The header that is tokenized + * @param delimiters The delimiters to be used + */ + public HeaderTokenizer(String header, String delimiters) { + this(header, delimiters, true); + } + + /** + * Constructor. The RFC822 defined delimiters - RFC822 - are + * used to delimit ATOMS. Also comments are skipped and not + * returned as tokens + * + * @param header the header string + */ + public HeaderTokenizer(String header) { + this(header, RFC822); + } + + /** + * Parses the next token from this String.

+ * + * Clients sit in a loop calling next() to parse successive + * tokens until an EOF Token is returned. + * + * @return the next Token + * @exception ParseException if the parse fails + */ + public Token next() throws ParseException { + return next('\0', false); + } + + /** + * Parses the next token from this String. + * If endOfAtom is not NUL, the token extends until the + * endOfAtom character is seen, or to the end of the header. + * This method is useful when parsing headers that don't + * obey the MIME specification, e.g., by failing to quote + * parameter values that contain spaces. + * + * @param endOfAtom if not NUL, character marking end of token + * @return the next Token + * @exception ParseException if the parse fails + * @since JavaMail 1.5 + */ + public Token next(char endOfAtom) throws ParseException { + return next(endOfAtom, false); + } + + /** + * Parses the next token from this String. + * endOfAtom is handled as above. If keepEscapes is true, + * any backslash escapes are preserved in the returned string. + * This method is useful when parsing headers that don't + * obey the MIME specification, e.g., by failing to escape + * backslashes in the filename parameter. + * + * @param endOfAtom if not NUL, character marking end of token + * @param keepEscapes keep all backslashes in returned string? + * @return the next Token + * @exception ParseException if the parse fails + * @since JavaMail 1.5 + */ + public Token next(char endOfAtom, boolean keepEscapes) + throws ParseException { + Token tk; + + currentPos = nextPos; // setup currentPos + tk = getNext(endOfAtom, keepEscapes); + nextPos = peekPos = currentPos; // update currentPos and peekPos + return tk; + } + + /** + * Peek at the next token, without actually removing the token + * from the parse stream. Invoking this method multiple times + * will return successive tokens, until next() is + * called.

+ * + * @return the next Token + * @exception ParseException if the parse fails + */ + public Token peek() throws ParseException { + Token tk; + + currentPos = peekPos; // setup currentPos + tk = getNext('\0', false); + peekPos = currentPos; // update peekPos + return tk; + } + + /** + * Return the rest of the Header. + * + * @return String rest of header. null is returned if we are + * already at end of header + */ + public String getRemainder() { + if (nextPos >= string.length()) + return null; + return string.substring(nextPos); + } + + /* + * Return the next token starting from 'currentPos'. After the + * parse, 'currentPos' is updated to point to the start of the + * next token. + */ + private Token getNext(char endOfAtom, boolean keepEscapes) + throws ParseException { + // If we're already at end of string, return EOF + if (currentPos >= maxPos) + return EOFToken; + + // Skip white-space, position currentPos beyond the space + if (skipWhiteSpace() == Token.EOF) + return EOFToken; + + char c; + int start; + boolean filter = false; + + c = string.charAt(currentPos); + + // Check or Skip comments and position currentPos + // beyond the comment + while (c == '(') { + // Parsing comment .. + int nesting; + for (start = ++currentPos, nesting = 1; + nesting > 0 && currentPos < maxPos; + currentPos++) { + c = string.charAt(currentPos); + if (c == '\\') { // Escape sequence + currentPos++; // skip the escaped character + filter = true; + } else if (c == '\r') + filter = true; + else if (c == '(') + nesting++; + else if (c == ')') + nesting--; + } + if (nesting != 0) + throw new ParseException("Unbalanced comments"); + + if (!skipComments) { + // Return the comment, if we are asked to. + // Note that the comment start & end markers are ignored. + String s; + if (filter) // need to go thru the token again. + s = filterToken(string, start, currentPos-1, keepEscapes); + else + s = string.substring(start,currentPos-1); + + return new Token(Token.COMMENT, s); + } + + // Skip any whitespace after the comment. + if (skipWhiteSpace() == Token.EOF) + return EOFToken; + c = string.charAt(currentPos); + } + + // Check for quoted-string and position currentPos + // beyond the terminating quote + if (c == '"') { + currentPos++; // skip initial quote + return collectString('"', keepEscapes); + } + + // Check for SPECIAL or CTL + if (c < 040 || c >= 0177 || delimiters.indexOf(c) >= 0) { + if (endOfAtom > 0 && c != endOfAtom) { + // not expecting a special character here, + // pretend it's a quoted string + return collectString(endOfAtom, keepEscapes); + } + currentPos++; // re-position currentPos + char ch[] = new char[1]; + ch[0] = c; + return new Token((int)c, new String(ch)); + } + + // Check for ATOM + for (start = currentPos; currentPos < maxPos; currentPos++) { + c = string.charAt(currentPos); + // ATOM is delimited by either SPACE, CTL, "(", <"> + // or the specified SPECIALS + if (c < 040 || c >= 0177 || c == '(' || c == ' ' || + c == '"' || delimiters.indexOf(c) >= 0) { + if (endOfAtom > 0 && c != endOfAtom) { + // not the expected atom after all; + // back up and pretend it's a quoted string + currentPos = start; + return collectString(endOfAtom, keepEscapes); + } + break; + } + } + return new Token(Token.ATOM, string.substring(start, currentPos)); + } + + private Token collectString(char eos, boolean keepEscapes) + throws ParseException { + int start; + boolean filter = false; + for (start = currentPos; currentPos < maxPos; currentPos++) { + char c = string.charAt(currentPos); + if (c == '\\') { // Escape sequence + currentPos++; + filter = true; + } else if (c == '\r') + filter = true; + else if (c == eos) { + currentPos++; + String s; + + if (filter) + s = filterToken(string, start, currentPos-1, keepEscapes); + else + s = string.substring(start, currentPos-1); + + if (c != '"') { // not a real quoted string + s = trimWhiteSpace(s); + currentPos--; // back up before the eos char + } + + return new Token(Token.QUOTEDSTRING, s); + } + } + + // ran off the end of the string + + // if we're looking for a matching quote, that's an error + if (eos == '"') + throw new ParseException("Unbalanced quoted string"); + + // otherwise, just return whatever's left + String s; + if (filter) + s = filterToken(string, start, currentPos, keepEscapes); + else + s = string.substring(start, currentPos); + s = trimWhiteSpace(s); + return new Token(Token.QUOTEDSTRING, s); + } + + // Skip SPACE, HT, CR and NL + private int skipWhiteSpace() { + char c; + for (; currentPos < maxPos; currentPos++) + if (((c = string.charAt(currentPos)) != ' ') && + (c != '\t') && (c != '\r') && (c != '\n')) + return currentPos; + return Token.EOF; + } + + // Trim SPACE, HT, CR and NL from end of string + private static String trimWhiteSpace(String s) { + char c; + int i; + for (i = s.length() - 1; i >= 0; i--) { + if (((c = s.charAt(i)) != ' ') && + (c != '\t') && (c != '\r') && (c != '\n')) + break; + } + if (i <= 0) + return ""; + else + return s.substring(0, i + 1); + } + + /* Process escape sequences and embedded LWSPs from a comment or + * quoted string. + */ + private static String filterToken(String s, int start, int end, + boolean keepEscapes) { + StringBuilder sb = new StringBuilder(); + char c; + boolean gotEscape = false; + boolean gotCR = false; + + for (int i = start; i < end; i++) { + c = s.charAt(i); + if (c == '\n' && gotCR) { + // This LF is part of an unescaped + // CRLF sequence (i.e, LWSP). Skip it. + gotCR = false; + continue; + } + + gotCR = false; + if (!gotEscape) { + // Previous character was NOT '\' + if (c == '\\') // skip this character + gotEscape = true; + else if (c == '\r') // skip this character + gotCR = true; + else // append this character + sb.append(c); + } else { + // Previous character was '\'. So no need to + // bother with any special processing, just + // append this character. If keepEscapes is + // set, keep the backslash. IE6 fails to escape + // backslashes in quoted strings in HTTP headers, + // e.g., in the filename parameter. + if (keepEscapes) + sb.append('\\'); + sb.append(c); + gotEscape = false; + } + } + return sb.toString(); + } +} diff --git a/app/src/main/java/javax/mail/internet/InternetAddress.java b/app/src/main/java/javax/mail/internet/InternetAddress.java new file mode 100644 index 0000000000..a271a5814d --- /dev/null +++ b/app/src/main/java/javax/mail/internet/InternetAddress.java @@ -0,0 +1,1525 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; +import java.util.ArrayList; +import java.util.StringTokenizer; +import java.util.Locale; +import java.nio.charset.StandardCharsets; +import javax.mail.*; +import com.sun.mail.util.PropUtil; + +/** + * This class represents an Internet email address using the syntax + * of RFC822. + * Typical address syntax is of the form "user@host.domain" or + * "Personal Name <user@host.domain>". + * + * @author Bill Shannon + * @author John Mani + */ + +public class InternetAddress extends Address implements Cloneable { + + protected String address; // email address + + /** + * The personal name. + */ + protected String personal; + + /** + * The RFC 2047 encoded version of the personal name.

+ * + * This field and the personal field track each + * other, so if a subclass sets one of these fields directly, it + * should set the other to null, so that it is + * suitably recomputed. + */ + protected String encodedPersonal; + + private static final long serialVersionUID = -7507595530758302903L; + + private static final boolean ignoreBogusGroupName = + PropUtil.getBooleanSystemProperty( + "mail.mime.address.ignorebogusgroupname", true); + + private static final boolean useCanonicalHostName = + PropUtil.getBooleanSystemProperty( + "mail.mime.address.usecanonicalhostname", true); + + private static final boolean allowUtf8 = + PropUtil.getBooleanSystemProperty("mail.mime.allowutf8", false); + + /** + * Default constructor. + */ + public InternetAddress() { } + + /** + * Constructor.

+ * + * Parse the given string and create an InternetAddress. + * See the parse method for details of the parsing. + * The address is parsed using "strict" parsing. + * This constructor does not perform the additional + * syntax checks that the + * InternetAddress(String address, boolean strict) + * constructor does when strict is true. + * This constructor is equivalent to + * InternetAddress(address, false). + * + * @param address the address in RFC822 format + * @exception AddressException if the parse failed + */ + public InternetAddress(String address) throws AddressException { + // use our address parsing utility routine to parse the string + InternetAddress a[] = parse(address, true); + // if we got back anything other than a single address, it's an error + if (a.length != 1) + throw new AddressException("Illegal address", address); + + /* + * Now copy the contents of the single address we parsed + * into the current object, which will be returned from the + * constructor. + * XXX - this sure is a round-about way of getting this done. + */ + this.address = a[0].address; + this.personal = a[0].personal; + this.encodedPersonal = a[0].encodedPersonal; + } + + /** + * Parse the given string and create an InternetAddress. + * If strict is false, the detailed syntax of the + * address isn't checked. + * + * @param address the address in RFC822 format + * @param strict enforce RFC822 syntax + * @exception AddressException if the parse failed + * @since JavaMail 1.3 + */ + public InternetAddress(String address, boolean strict) + throws AddressException { + this(address); + if (strict) { + if (isGroup()) + getGroup(true); // throw away the result + else + checkAddress(this.address, true, true); + } + } + + /** + * Construct an InternetAddress given the address and personal name. + * The address is assumed to be a syntactically valid RFC822 address. + * + * @param address the address in RFC822 format + * @param personal the personal name + * @exception UnsupportedEncodingException if the personal name + * can't be encoded in the given charset + */ + public InternetAddress(String address, String personal) + throws UnsupportedEncodingException { + this(address, personal, null); + } + + /** + * Construct an InternetAddress given the address and personal name. + * The address is assumed to be a syntactically valid RFC822 address. + * + * @param address the address in RFC822 format + * @param personal the personal name + * @param charset the MIME charset for the name + * @exception UnsupportedEncodingException if the personal name + * can't be encoded in the given charset + */ + public InternetAddress(String address, String personal, String charset) + throws UnsupportedEncodingException { + this.address = address; + setPersonal(personal, charset); + } + + /** + * Return a copy of this InternetAddress object. + * @since JavaMail 1.2 + */ + @Override + public Object clone() { + InternetAddress a = null; + try { + a = (InternetAddress)super.clone(); + } catch (CloneNotSupportedException e) {} // Won't happen + return a; + } + + /** + * Return the type of this address. The type of an InternetAddress + * is "rfc822". + */ + @Override + public String getType() { + return "rfc822"; + } + + /** + * Set the email address. + * + * @param address email address + */ + public void setAddress(String address) { + this.address = address; + } + + /** + * Set the personal name. If the name contains non US-ASCII + * characters, then the name will be encoded using the specified + * charset as per RFC 2047. If the name contains only US-ASCII + * characters, no encoding is done and the name is used as is.

+ * + * @param name personal name + * @param charset MIME charset to be used to encode the name as + * per RFC 2047 + * @see #setPersonal(String) + * @exception UnsupportedEncodingException if the charset encoding + * fails. + */ + public void setPersonal(String name, String charset) + throws UnsupportedEncodingException { + personal = name; + if (name != null) + encodedPersonal = MimeUtility.encodeWord(name, charset, null); + else + encodedPersonal = null; + } + + /** + * Set the personal name. If the name contains non US-ASCII + * characters, then the name will be encoded using the platform's + * default charset. If the name contains only US-ASCII characters, + * no encoding is done and the name is used as is.

+ * + * @param name personal name + * @see #setPersonal(String name, String charset) + * @exception UnsupportedEncodingException if the charset encoding + * fails. + */ + public void setPersonal(String name) + throws UnsupportedEncodingException { + personal = name; + if (name != null) + encodedPersonal = MimeUtility.encodeWord(name); + else + encodedPersonal = null; + } + + /** + * Get the email address. + * @return email address + */ + public String getAddress() { + return address; + } + + /** + * Get the personal name. If the name is encoded as per RFC 2047, + * it is decoded and converted into Unicode. If the decoding or + * conversion fails, the raw data is returned as is. + * + * @return personal name + */ + public String getPersonal() { + if (personal != null) + return personal; + + if (encodedPersonal != null) { + try { + personal = MimeUtility.decodeText(encodedPersonal); + return personal; + } catch (Exception ex) { + // 1. ParseException: either its an unencoded string or + // it can't be parsed + // 2. UnsupportedEncodingException: can't decode it. + return encodedPersonal; + } + } + // No personal or encodedPersonal, return null + return null; + } + + /** + * Convert this address into a RFC 822 / RFC 2047 encoded address. + * The resulting string contains only US-ASCII characters, and + * hence is mail-safe. + * + * @return possibly encoded address string + */ + @Override + public String toString() { + String a = address == null ? "" : address; + if (encodedPersonal == null && personal != null) + try { + encodedPersonal = MimeUtility.encodeWord(personal); + } catch (UnsupportedEncodingException ex) { } + + if (encodedPersonal != null) + return quotePhrase(encodedPersonal) + " <" + a + ">"; + else if (isGroup() || isSimple()) + return a; + else + return "<" + a + ">"; + } + + /** + * Returns a properly formatted address (RFC 822 syntax) of + * Unicode characters. + * + * @return Unicode address string + * @since JavaMail 1.2 + */ + public String toUnicodeString() { + String p = getPersonal(); + if (p != null) + return quotePhrase(p) + " <" + address + ">"; + else if (isGroup() || isSimple()) + return address; + else + return "<" + address + ">"; + } + + /* + * quotePhrase() quotes the words within a RFC822 phrase. + * + * This is tricky, since a phrase is defined as 1 or more + * RFC822 words, separated by LWSP. Now, a word that contains + * LWSP is supposed to be quoted, and this is exactly what the + * MimeUtility.quote() method does. However, when dealing with + * a phrase, any LWSP encountered can be construed to be the + * separator between words, and not part of the words themselves. + * To deal with this funkiness, we have the below variant of + * MimeUtility.quote(), which essentially ignores LWSP when + * deciding whether to quote a word. + * + * It aint pretty, but it gets the job done :) + */ + + private static final String rfc822phrase = + HeaderTokenizer.RFC822.replace(' ', '\0').replace('\t', '\0'); + + private static String quotePhrase(String phrase) { + int len = phrase.length(); + boolean needQuoting = false; + + for (int i = 0; i < len; i++) { + char c = phrase.charAt(i); + if (c == '"' || c == '\\') { + // need to escape them and then quote the whole string + StringBuilder sb = new StringBuilder(len + 3); + sb.append('"'); + for (int j = 0; j < len; j++) { + char cc = phrase.charAt(j); + if (cc == '"' || cc == '\\') + // Escape the character + sb.append('\\'); + sb.append(cc); + } + sb.append('"'); + return sb.toString(); + } else if ((c < 040 && c != '\r' && c != '\n' && c != '\t') || + (c >= 0177 && !allowUtf8) || rfc822phrase.indexOf(c) >= 0) + // These characters cause the string to be quoted + needQuoting = true; + } + + if (needQuoting) { + StringBuilder sb = new StringBuilder(len + 2); + sb.append('"').append(phrase).append('"'); + return sb.toString(); + } else + return phrase; + } + + private static String unquote(String s) { + if (s.startsWith("\"") && s.endsWith("\"") && s.length() > 1) { + s = s.substring(1, s.length() - 1); + // check for any escaped characters + if (s.indexOf('\\') >= 0) { + StringBuilder sb = new StringBuilder(s.length()); // approx + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\\' && i < s.length() - 1) + c = s.charAt(++i); + sb.append(c); + } + s = sb.toString(); + } + } + return s; + } + + /** + * The equality operator. + */ + @Override + public boolean equals(Object a) { + if (!(a instanceof InternetAddress)) + return false; + + String s = ((InternetAddress)a).getAddress(); + if (s == address) + return true; + if (address != null && address.equalsIgnoreCase(s)) + return true; + + return false; + } + + /** + * Compute a hash code for the address. + */ + @Override + public int hashCode() { + if (address == null) + return 0; + else + return address.toLowerCase(Locale.ENGLISH).hashCode(); + } + + /** + * Convert the given array of InternetAddress objects into + * a comma separated sequence of address strings. The + * resulting string contains only US-ASCII characters, and + * hence is mail-safe.

+ * + * @param addresses array of InternetAddress objects + * @exception ClassCastException if any address object in the + * given array is not an InternetAddress object. Note + * that this is a RuntimeException. + * @return comma separated string of addresses + */ + public static String toString(Address[] addresses) { + return toString(addresses, 0); + } + + /** + * Convert the given array of InternetAddress objects into + * a comma separated sequence of address strings. The + * resulting string contains Unicode characters.

+ * + * @param addresses array of InternetAddress objects + * @exception ClassCastException if any address object in the + * given array is not an InternetAddress object. Note + * that this is a RuntimeException. + * @return comma separated string of addresses + * @since JavaMail 1.6 + */ + public static String toUnicodeString(Address[] addresses) { + return toUnicodeString(addresses, 0); + } + + /** + * Convert the given array of InternetAddress objects into + * a comma separated sequence of address strings. The + * resulting string contains only US-ASCII characters, and + * hence is mail-safe.

+ * + * The 'used' parameter specifies the number of character positions + * already taken up in the field into which the resulting address + * sequence string is to be inserted. It is used to determine the + * line-break positions in the resulting address sequence string. + * + * @param addresses array of InternetAddress objects + * @param used number of character positions already used, in + * the field into which the address string is to + * be inserted. + * @exception ClassCastException if any address object in the + * given array is not an InternetAddress object. Note + * that this is a RuntimeException. + * @return comma separated string of addresses + */ + public static String toString(Address[] addresses, int used) { + if (addresses == null || addresses.length == 0) + return null; + + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < addresses.length; i++) { + if (i != 0) { // need to append comma + sb.append(", "); + used += 2; + } + + // prefer not to split a single address across lines so used=0 below + String s = MimeUtility.fold(0, addresses[i].toString()); + int len = lengthOfFirstSegment(s); // length till CRLF + if (used + len > 76) { // overflows ... + // smash trailing space from ", " above + int curlen = sb.length(); + if (curlen > 0 && sb.charAt(curlen - 1) == ' ') + sb.setLength(curlen - 1); + sb.append("\r\n\t"); // .. start new continuation line + used = 8; // account for the starting char + } + sb.append(s); + used = lengthOfLastSegment(s, used); + } + + return sb.toString(); + } + + /** + * Convert the given array of InternetAddress objects into + * a comma separated sequence of address strings. The + * resulting string contains Unicode characters.

+ * + * The 'used' parameter specifies the number of character positions + * already taken up in the field into which the resulting address + * sequence string is to be inserted. It is used to determine the + * line-break positions in the resulting address sequence string. + * + * @param addresses array of InternetAddress objects + * @param used number of character positions already used, in + * the field into which the address string is to + * be inserted. + * @exception ClassCastException if any address object in the + * given array is not an InternetAddress object. Note + * that this is a RuntimeException. + * @return comma separated string of addresses + * @since JavaMail 1.6 + */ + /* + * XXX - This is exactly the same as the above, except it uses + * toUnicodeString instead of toString. + * XXX - Since the line length restrictions are in bytes, not characters, + * we convert all non-ASCII addresses to UTF-8 byte strings, + * which we then convert to ISO-8859-1 Strings where every + * character respresents one UTF-8 byte. At the end we reverse + * the conversion to get back to a correct Unicode string. + * This is a hack to allow all the other character-based methods + * to work properly with UTF-8 bytes. + */ + public static String toUnicodeString(Address[] addresses, int used) { + if (addresses == null || addresses.length == 0) + return null; + + StringBuilder sb = new StringBuilder(); + + boolean sawNonAscii = false; + for (int i = 0; i < addresses.length; i++) { + if (i != 0) { // need to append comma + sb.append(", "); + used += 2; + } + + // prefer not to split a single address across lines so used=0 below + String as = ((InternetAddress)addresses[i]).toUnicodeString(); + if (MimeUtility.checkAscii(as) != MimeUtility.ALL_ASCII) { + sawNonAscii = true; + as = new String(as.getBytes(StandardCharsets.UTF_8), + StandardCharsets.ISO_8859_1); + } + String s = MimeUtility.fold(0, as); + int len = lengthOfFirstSegment(s); // length till CRLF + if (used + len > 76) { // overflows ... + // smash trailing space from ", " above + int curlen = sb.length(); + if (curlen > 0 && sb.charAt(curlen - 1) == ' ') + sb.setLength(curlen - 1); + sb.append("\r\n\t"); // .. start new continuation line + used = 8; // account for the starting char + } + sb.append(s); + used = lengthOfLastSegment(s, used); + } + + String ret = sb.toString(); + if (sawNonAscii) + ret = new String(ret.getBytes(StandardCharsets.ISO_8859_1), + StandardCharsets.UTF_8); + return ret; + } + + /* + * Return the length of the first segment within this string. + * If no segments exist, the length of the whole line is returned. + */ + private static int lengthOfFirstSegment(String s) { + int pos; + if ((pos = s.indexOf("\r\n")) != -1) + return pos; + else + return s.length(); + } + + /* + * Return the length of the last segment within this string. + * If no segments exist, the length of the whole line plus + * used is returned. + */ + private static int lengthOfLastSegment(String s, int used) { + int pos; + if ((pos = s.lastIndexOf("\r\n")) != -1) + return s.length() - pos - 2; + else + return s.length() + used; + } + + /** + * Return an InternetAddress object representing the current user. + * The entire email address may be specified in the "mail.from" + * property. If not set, the "mail.user" and "mail.host" properties + * are tried. If those are not set, the "user.name" property and + * InetAddress.getLocalHost method are tried. + * Security exceptions that may occur while accessing this information + * are ignored. If it is not possible to determine an email address, + * null is returned. + * + * @param session Session object used for property lookup + * @return current user's email address + */ + public static InternetAddress getLocalAddress(Session session) { + try { + return _getLocalAddress(session); + } catch (SecurityException sex) { // ignore it + } catch (AddressException ex) { // ignore it + } catch (UnknownHostException ex) { } // ignore it + return null; + } + + /** + * A package-private version of getLocalAddress that doesn't swallow + * the exception. Used by MimeMessage.setFrom() to report the reason + * for the failure. + */ + // package-private + static InternetAddress _getLocalAddress(Session session) + throws SecurityException, AddressException, UnknownHostException { + String user = null, host = null, address = null; + if (session == null) { + user = System.getProperty("user.name"); + host = getLocalHostName(); + } else { + address = session.getProperty("mail.from"); + if (address == null) { + user = session.getProperty("mail.user"); + if (user == null || user.length() == 0) + user = session.getProperty("user.name"); + if (user == null || user.length() == 0) + user = System.getProperty("user.name"); + host = session.getProperty("mail.host"); + if (host == null || host.length() == 0) + host = getLocalHostName(); + } + } + + if (address == null && user != null && user.length() != 0 && + host != null && host.length() != 0) + address = MimeUtility.quote(user.trim(), specialsNoDot + "\t ") + + "@" + host; + + if (address == null) + return null; + + return new InternetAddress(address); + } + + /** + * Get the local host name from InetAddress and return it in a form + * suitable for use in an email address. + */ + private static String getLocalHostName() throws UnknownHostException { + String host = null; + InetAddress me = InetAddress.getLocalHost(); + if (me != null) { + // try canonical host name first + if (useCanonicalHostName) + host = me.getCanonicalHostName(); + if (host == null) + host = me.getHostName(); + // if we can't get our name, use local address literal + if (host == null) + host = me.getHostAddress(); + if (host != null && host.length() > 0 && isInetAddressLiteral(host)) + host = '[' + host + ']'; + } + return host; + } + + /** + * Is the address an IPv4 or IPv6 address literal, which needs to + * be enclosed in "[]" in an email address? IPv4 literals contain + * decimal digits and dots, IPv6 literals contain hex digits, dots, + * and colons. We're lazy and don't check the exact syntax, just + * the allowed characters; strings that have only the allowed + * characters in a literal but don't meet the syntax requirements + * for a literal definitely can't be a host name and thus will fail + * later when used as an address literal. + */ + private static boolean isInetAddressLiteral(String addr) { + boolean sawHex = false, sawColon = false; + for (int i = 0; i < addr.length(); i++) { + char c = addr.charAt(i); + if (c >= '0' && c <= '9') + ; // digits always ok + else if (c == '.') + ; // dot always ok + else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) + sawHex = true; // need to see a colon too + else if (c == ':') + sawColon = true; + else + return false; // anything else, definitely not a literal + } + return !sawHex || sawColon; + } + + /** + * Parse the given comma separated sequence of addresses into + * InternetAddress objects. Addresses must follow RFC822 syntax. + * + * @param addresslist comma separated address strings + * @return array of InternetAddress objects + * @exception AddressException if the parse failed + */ + public static InternetAddress[] parse(String addresslist) + throws AddressException { + return parse(addresslist, true); + } + + /** + * Parse the given sequence of addresses into InternetAddress + * objects. If strict is false, simple email addresses + * separated by spaces are also allowed. If strict is + * true, many (but not all) of the RFC822 syntax rules are enforced. + * In particular, even if strict is true, addresses + * composed of simple names (with no "@domain" part) are allowed. + * Such "illegal" addresses are not uncommon in real messages.

+ * + * Non-strict parsing is typically used when parsing a list of + * mail addresses entered by a human. Strict parsing is typically + * used when parsing address headers in mail messages. + * + * @param addresslist comma separated address strings + * @param strict enforce RFC822 syntax + * @return array of InternetAddress objects + * @exception AddressException if the parse failed + */ + public static InternetAddress[] parse(String addresslist, boolean strict) + throws AddressException { + return parse(addresslist, strict, false); + } + + /** + * Parse the given sequence of addresses into InternetAddress + * objects. If strict is false, the full syntax rules for + * individual addresses are not enforced. If strict is + * true, many (but not all) of the RFC822 syntax rules are enforced.

+ * + * To better support the range of "invalid" addresses seen in real + * messages, this method enforces fewer syntax rules than the + * parse method when the strict flag is false + * and enforces more rules when the strict flag is true. If the + * strict flag is false and the parse is successful in separating out an + * email address or addresses, the syntax of the addresses themselves + * is not checked. + * + * @param addresslist comma separated address strings + * @param strict enforce RFC822 syntax + * @return array of InternetAddress objects + * @exception AddressException if the parse failed + * @since JavaMail 1.3 + */ + public static InternetAddress[] parseHeader(String addresslist, + boolean strict) throws AddressException { + return parse(MimeUtility.unfold(addresslist), strict, true); + } + + /* + * RFC822 Address parser. + * + * XXX - This is complex enough that it ought to be a real parser, + * not this ad-hoc mess, and because of that, this is not perfect. + * + * XXX - Deal with encoded Headers too. + */ + @SuppressWarnings("fallthrough") + private static InternetAddress[] parse(String s, boolean strict, + boolean parseHdr) throws AddressException { + int start, end, index, nesting; + int start_personal = -1, end_personal = -1; + int length = s.length(); + boolean ignoreErrors = parseHdr && !strict; + boolean in_group = false; // we're processing a group term + boolean route_addr = false; // address came from route-addr term + boolean rfc822 = false; // looks like an RFC822 address + char c; + List v = new ArrayList<>(); + InternetAddress ma; + + for (start = end = -1, index = 0; index < length; index++) { + c = s.charAt(index); + + switch (c) { + case '(': // We are parsing a Comment. Ignore everything inside. + // XXX - comment fields should be parsed as whitespace, + // more than one allowed per address + rfc822 = true; + if (start >= 0 && end == -1) + end = index; + int pindex = index; + for (index++, nesting = 1; index < length && nesting > 0; + index++) { + c = s.charAt(index); + switch (c) { + case '\\': + index++; // skip both '\' and the escaped char + break; + case '(': + nesting++; + break; + case ')': + nesting--; + break; + default: + break; + } + } + if (nesting > 0) { + if (!ignoreErrors) + throw new AddressException("Missing ')'", s, index); + // pretend the first paren was a regular character and + // continue parsing after it + index = pindex + 1; + break; + } + index--; // point to closing paren + if (start_personal == -1) + start_personal = pindex + 1; + if (end_personal == -1) + end_personal = index; + break; + + case ')': + if (!ignoreErrors) + throw new AddressException("Missing '('", s, index); + // pretend the left paren was a regular character and + // continue parsing + if (start == -1) + start = index; + break; + + case '<': + rfc822 = true; + if (route_addr) { + if (!ignoreErrors) + throw new AddressException( + "Extra route-addr", s, index); + + // assume missing comma between addresses + if (start == -1) { + route_addr = false; + rfc822 = false; + start = end = -1; + break; // nope, nothing there + } + if (!in_group) { + // got a token, add this to our InternetAddress list + if (end == -1) // should never happen + end = index; + String addr = s.substring(start, end).trim(); + + ma = new InternetAddress(); + ma.setAddress(addr); + if (start_personal >= 0) { + ma.encodedPersonal = unquote( + s.substring(start_personal, end_personal). + trim()); + } + v.add(ma); + + route_addr = false; + rfc822 = false; + start = end = -1; + start_personal = end_personal = -1; + // continue processing this new address... + } + } + + int rindex = index; + boolean inquote = false; + outf: + for (index++; index < length; index++) { + c = s.charAt(index); + switch (c) { + case '\\': // XXX - is this needed? + index++; // skip both '\' and the escaped char + break; + case '"': + inquote = !inquote; + break; + case '>': + if (inquote) + continue; + break outf; // out of for loop + default: + break; + } + } + + // did we find a matching quote? + if (inquote) { + if (!ignoreErrors) + throw new AddressException("Missing '\"'", s, index); + // didn't find matching quote, try again ignoring quotes + // (e.g., ``<"@foo.com>'') + outq: + for (index = rindex + 1; index < length; index++) { + c = s.charAt(index); + if (c == '\\') // XXX - is this needed? + index++; // skip both '\' and the escaped char + else if (c == '>') + break; + } + } + + // did we find a terminating '>'? + if (index >= length) { + if (!ignoreErrors) + throw new AddressException("Missing '>'", s, index); + // pretend the "<" was a regular character and + // continue parsing after it (e.g., ``<@foo.com'') + index = rindex + 1; + if (start == -1) + start = rindex; // back up to include "<" + break; + } + + if (!in_group) { + if (start >= 0) { + // seen some characters? use them as the personal name + start_personal = start; + end_personal = rindex; + } + start = rindex + 1; + } + route_addr = true; + end = index; + break; + + case '>': + if (!ignoreErrors) + throw new AddressException("Missing '<'", s, index); + // pretend the ">" was a regular character and + // continue parsing (e.g., ``>@foo.com'') + if (start == -1) + start = index; + break; + + case '"': // parse quoted string + int qindex = index; + rfc822 = true; + if (start == -1) + start = index; + outq: + for (index++; index < length; index++) { + c = s.charAt(index); + switch (c) { + case '\\': + index++; // skip both '\' and the escaped char + break; + case '"': + break outq; // out of for loop + default: + break; + } + } + if (index >= length) { + if (!ignoreErrors) + throw new AddressException("Missing '\"'", s, index); + // pretend the quote was a regular character and + // continue parsing after it (e.g., ``"@foo.com'') + index = qindex + 1; + } + break; + + case '[': // a domain-literal, probably + int lindex = index; + rfc822 = true; + if (start == -1) + start = index; + outb: + for (index++; index < length; index++) { + c = s.charAt(index); + switch (c) { + case '\\': + index++; // skip both '\' and the escaped char + break; + case ']': + break outb; // out of for loop + default: + break; + } + } + if (index >= length) { + if (!ignoreErrors) + throw new AddressException("Missing ']'", s, index); + // pretend the "[" was a regular character and + // continue parsing after it (e.g., ``[@foo.com'') + index = lindex + 1; + } + break; + + case ';': + if (start == -1) { + route_addr = false; + rfc822 = false; + start = end = -1; + break; // nope, nothing there + } + if (in_group) { + in_group = false; + /* + * If parsing headers, but not strictly, peek ahead. + * If next char is "@", treat the group name + * like the local part of the address, e.g., + * "Undisclosed-Recipient:;@java.sun.com". + */ + if (parseHdr && !strict && + index + 1 < length && s.charAt(index + 1) == '@') + break; + ma = new InternetAddress(); + end = index + 1; + ma.setAddress(s.substring(start, end).trim()); + v.add(ma); + + route_addr = false; + rfc822 = false; + start = end = -1; + start_personal = end_personal = -1; + break; + } + if (!ignoreErrors) + throw new AddressException( + "Illegal semicolon, not in group", s, index); + + // otherwise, parsing a header; treat semicolon like comma + // fall through to comma case... + + case ',': // end of an address, probably + if (start == -1) { + route_addr = false; + rfc822 = false; + start = end = -1; + break; // nope, nothing there + } + if (in_group) { + route_addr = false; + break; + } + // got a token, add this to our InternetAddress list + if (end == -1) + end = index; + + String addr = s.substring(start, end).trim(); + String pers = null; + if (rfc822 && start_personal >= 0) { + pers = unquote( + s.substring(start_personal, end_personal).trim()); + if (pers.trim().length() == 0) + pers = null; + } + + /* + * If the personal name field has an "@" and the address + * field does not, assume they were reversed, e.g., + * ``"joe doe" (john.doe@example.com)''. + */ + if (parseHdr && !strict && pers != null && + pers.indexOf('@') >= 0 && + addr.indexOf('@') < 0 && addr.indexOf('!') < 0) { + String tmp = addr; + addr = pers; + pers = tmp; + } + if (rfc822 || strict || parseHdr) { + if (!ignoreErrors) + checkAddress(addr, route_addr, false); + ma = new InternetAddress(); + ma.setAddress(addr); + if (pers != null) + ma.encodedPersonal = pers; + v.add(ma); + } else { + // maybe we passed over more than one space-separated addr + StringTokenizer st = new StringTokenizer(addr); + while (st.hasMoreTokens()) { + String a = st.nextToken(); + checkAddress(a, false, false); + ma = new InternetAddress(); + ma.setAddress(a); + v.add(ma); + } + } + + route_addr = false; + rfc822 = false; + start = end = -1; + start_personal = end_personal = -1; + break; + + case ':': + rfc822 = true; + if (in_group) + if (!ignoreErrors) + throw new AddressException("Nested group", s, index); + if (start == -1) + start = index; + if (parseHdr && !strict) { + /* + * If next char is a special character that can't occur at + * the start of a valid address, treat the group name + * as the entire address, e.g., "Date:, Tue", "Re:@foo". + */ + if (index + 1 < length) { + String addressSpecials = ")>[]:@\\,."; + char nc = s.charAt(index + 1); + if (addressSpecials.indexOf(nc) >= 0) { + if (nc != '@') + break; // don't change in_group + /* + * Handle a common error: + * ``Undisclosed-Recipient:@example.com;'' + * + * Scan ahead. If we find a semicolon before + * one of these other special characters, + * consider it to be a group after all. + */ + for (int i = index + 2; i < length; i++) { + nc = s.charAt(i); + if (nc == ';') + break; + if (addressSpecials.indexOf(nc) >= 0) + break; + } + if (nc == ';') + break; // don't change in_group + } + } + + // ignore bogus "mailto:" prefix in front of an address, + // or bogus mail header name included in the address field + String gname = s.substring(start, index); + if (ignoreBogusGroupName && + (gname.equalsIgnoreCase("mailto") || + gname.equalsIgnoreCase("From") || + gname.equalsIgnoreCase("To") || + gname.equalsIgnoreCase("Cc") || + gname.equalsIgnoreCase("Subject") || + gname.equalsIgnoreCase("Re"))) + start = -1; // we're not really in a group + else + in_group = true; + } else + in_group = true; + break; + + // Ignore whitespace + case ' ': + case '\t': + case '\r': + case '\n': + break; + + default: + if (start == -1) + start = index; + break; + } + } + + if (start >= 0) { + /* + * The last token, add this to our InternetAddress list. + * Note that this block of code should be identical to the + * block above for "case ','". + */ + if (end == -1) + end = length; + + String addr = s.substring(start, end).trim(); + String pers = null; + if (rfc822 && start_personal >= 0) { + pers = unquote( + s.substring(start_personal, end_personal).trim()); + if (pers.trim().length() == 0) + pers = null; + } + + /* + * If the personal name field has an "@" and the address + * field does not, assume they were reversed, e.g., + * ``"joe doe" (john.doe@example.com)''. + */ + if (parseHdr && !strict && + pers != null && pers.indexOf('@') >= 0 && + addr.indexOf('@') < 0 && addr.indexOf('!') < 0) { + String tmp = addr; + addr = pers; + pers = tmp; + } + if (rfc822 || strict || parseHdr) { + if (!ignoreErrors) + checkAddress(addr, route_addr, false); + ma = new InternetAddress(); + ma.setAddress(addr); + if (pers != null) + ma.encodedPersonal = pers; + v.add(ma); + } else { + // maybe we passed over more than one space-separated addr + StringTokenizer st = new StringTokenizer(addr); + while (st.hasMoreTokens()) { + String a = st.nextToken(); + checkAddress(a, false, false); + ma = new InternetAddress(); + ma.setAddress(a); + v.add(ma); + } + } + } + + InternetAddress[] a = new InternetAddress[v.size()]; + v.toArray(a); + return a; + } + + /** + * Validate that this address conforms to the syntax rules of + * RFC 822. The current implementation checks many, but not + * all, syntax rules. Note that even though the syntax of + * the address may be correct, there's no guarantee that a + * mailbox of that name exists. + * + * @exception AddressException if the address isn't valid. + * @since JavaMail 1.3 + */ + public void validate() throws AddressException { + if (isGroup()) + getGroup(true); // throw away the result + else + checkAddress(getAddress(), true, true); + } + + private static final String specialsNoDotNoAt = "()<>,;:\\\"[]"; + private static final String specialsNoDot = specialsNoDotNoAt + "@"; + + /** + * Check that the address is a valid "mailbox" per RFC822. + * (We also allow simple names.) + * + * XXX - much more to check + * XXX - doesn't handle domain-literals properly (but no one uses them) + */ + private static void checkAddress(String addr, + boolean routeAddr, boolean validate) + throws AddressException { + int i, start = 0; + + if (addr == null) + throw new AddressException("Address is null"); + int len = addr.length(); + if (len == 0) + throw new AddressException("Empty address", addr); + + /* + * routeAddr indicates that the address is allowed + * to have an RFC 822 "route". + */ + if (routeAddr && addr.charAt(0) == '@') { + /* + * Check for a legal "route-addr": + * [@domain[,@domain ...]:]local@domain + */ + for (start = 0; (i = indexOfAny(addr, ",:", start)) >= 0; + start = i+1) { + if (addr.charAt(start) != '@') + throw new AddressException("Illegal route-addr", addr); + if (addr.charAt(i) == ':') { + // end of route-addr + start = i + 1; + break; + } + } + } + + /* + * The rest should be "local@domain", but we allow simply "local" + * unless called from validate. + * + * local-part must follow RFC 822 - no specials except '.' + * unless quoted. + */ + + char c = (char)-1; + char lastc = (char)-1; + boolean inquote = false; + for (i = start; i < len; i++) { + lastc = c; + c = addr.charAt(i); + // a quoted-pair is only supposed to occur inside a quoted string, + // but some people use it outside so we're more lenient + if (c == '\\' || lastc == '\\') + continue; + if (c == '"') { + if (inquote) { + // peek ahead, next char must be "@" + if (validate && i + 1 < len && addr.charAt(i + 1) != '@') + throw new AddressException( + "Quote not at end of local address", addr); + inquote = false; + } else { + if (validate && i != 0) + throw new AddressException( + "Quote not at start of local address", addr); + inquote = true; + } + continue; + } else if (c == '\r') { + // peek ahead, next char must be LF + if (i + 1 < len && addr.charAt(i + 1) != '\n') + throw new AddressException( + "Quoted local address contains CR without LF", addr); + } else if (c == '\n') { + /* + * CRLF followed by whitespace is allowed in a quoted string. + * We allowed naked LF, but ensure LF is always followed by + * whitespace to prevent spoofing the end of the header. + */ + if (i + 1 < len && addr.charAt(i + 1) != ' ' && + addr.charAt(i + 1) != '\t') + throw new AddressException( + "Quoted local address contains newline without whitespace", + addr); + } + if (inquote) + continue; + // dot rules should not be applied to quoted-string + if (c == '.') { + if (i == start) + throw new AddressException( + "Local address starts with dot", addr); + if (lastc == '.') + throw new AddressException( + "Local address contains dot-dot", addr); + } + if (c == '@') { + if (i == 0) + throw new AddressException("Missing local name", addr); + if (lastc == '.') + throw new AddressException( + "Local address ends with dot", addr); + break; // done with local part + } + if (c <= 040 || c == 0177) + throw new AddressException( + "Local address contains control or whitespace", addr); + if (specialsNoDot.indexOf(c) >= 0) + throw new AddressException( + "Local address contains illegal character", addr); + } + if (inquote) + throw new AddressException("Unterminated quote", addr); + + /* + * Done with local part, now check domain. + * + * Note that the MimeMessage class doesn't remember addresses + * as separate objects; it writes them out as headers and then + * parses the headers when the addresses are requested. + * In order to support the case where a "simple" address is used, + * but the address also has a personal name and thus looks like + * it should be a valid RFC822 address when parsed, we only check + * this if we're explicitly called from the validate method. + */ + + if (c != '@') { + if (validate) + throw new AddressException("Missing final '@domain'", addr); + return; + } + + // check for illegal chars in the domain, but ignore domain literals + + start = i + 1; + if (start >= len) + throw new AddressException("Missing domain", addr); + + if (addr.charAt(start) == '.') + throw new AddressException("Domain starts with dot", addr); + boolean inliteral = false; + for (i = start; i < len; i++) { + c = addr.charAt(i); + if (c == '[') { + if (i != start) + throw new AddressException( + "Domain literal not at start of domain", addr); + inliteral = true; // domain literal, don't validate + } else if (c == ']') { + if (i != len - 1) + throw new AddressException( + "Domain literal end not at end of domain", addr); + inliteral = false; + } else if (c <= 040 || c == 0177) { + throw new AddressException( + "Domain contains control or whitespace", addr); + } else { + // RFC 2822 rule + //if (specialsNoDot.indexOf(c) >= 0) + /* + * RFC 1034 rule is more strict + * the full rule is: + * + * ::= | " " + * ::=

+ * + * This class is mostly intended for service providers. MimeMessage + * and MimeBody use this class for holding their headers. + * + *


A note on RFC822 and MIME headers

+ * + * RFC822 and MIME header fields must contain only + * US-ASCII characters. If a header contains non US-ASCII characters, + * it must be encoded as per the rules in RFC 2047. The MimeUtility + * class provided in this package can be used to to achieve this. + * Callers of the setHeader, addHeader, and + * addHeaderLine methods are responsible for enforcing + * the MIME requirements for the specified headers. In addition, these + * header fields must be folded (wrapped) before being sent if they + * exceed the line length limitation for the transport (1000 bytes for + * SMTP). Received headers may have been folded. The application is + * responsible for folding and unfolding headers as appropriate.

+ * + * The current implementation supports the System property + * mail.mime.ignorewhitespacelines, which if set to true + * will cause a line containing only whitespace to be considered + * a blank line terminating the header. + * + * @see javax.mail.internet.MimeUtility + * @author John Mani + * @author Bill Shannon + */ + +public class InternetHeaders { + private static final boolean ignoreWhitespaceLines = + PropUtil.getBooleanSystemProperty("mail.mime.ignorewhitespacelines", + false); + + /** + * An individual internet header. This class is only used by + * subclasses of InternetHeaders.

+ * + * An InternetHeader object with a null value is used as a placeholder + * for headers of that name, to preserve the order of headers. + * A placeholder InternetHeader object with a name of ":" marks + * the location in the list of headers where new headers are + * added by default. + * + * @since JavaMail 1.4 + */ + protected static final class InternetHeader extends Header { + /* + * Note that the value field from the superclass + * isn't used in this class. We extract the value + * from the line field as needed. We store the line + * rather than just the value to ensure that we can + * get back the exact original line, with the original + * whitespace, etc. + */ + String line; // the entire RFC822 header "line", + // or null if placeholder + + /** + * Constructor that takes a line and splits out + * the header name. + * + * @param l the header line + */ + public InternetHeader(String l) { + super("", ""); // XXX - we'll change it later + int i = l.indexOf(':'); + if (i < 0) { + // should never happen + name = l.trim(); + } else { + name = l.substring(0, i).trim(); + } + line = l; + } + + /** + * Constructor that takes a header name and value. + * + * @param n the name of the header + * @param v the value of the header + */ + public InternetHeader(String n, String v) { + super(n, ""); + if (v != null) + line = n + ": " + v; + else + line = null; + } + + /** + * Return the "value" part of the header line. + */ + @Override + public String getValue() { + int i = line.indexOf(':'); + if (i < 0) + return line; + // skip whitespace after ':' + int j; + for (j = i + 1; j < line.length(); j++) { + char c = line.charAt(j); + if (!(c == ' ' || c == '\t' || c == '\r' || c == '\n')) + break; + } + return line.substring(j); + } + } + + /* + * The enumeration object used to enumerate an + * InternetHeaders object. Can return + * either a String or a Header object. + */ + static class MatchEnum { + private Iterator e; // enum object of headers List + // XXX - is this overkill? should we step through in index + // order instead? + private String names[]; // names to match, or not + private boolean match; // return matching headers? + private boolean want_line; // return header lines? + private InternetHeader next_header; // the next header to be returned + + /* + * Constructor. Initialize the enumeration for the entire + * List of headers, the set of headers, whether to return + * matching or non-matching headers, and whether to return + * header lines or Header objects. + */ + MatchEnum(List v, String n[], boolean m, boolean l) { + e = v.iterator(); + names = n; + match = m; + want_line = l; + next_header = null; + } + + /* + * Any more elements in this enumeration? + */ + public boolean hasMoreElements() { + // if necessary, prefetch the next matching header, + // and remember it. + if (next_header == null) + next_header = nextMatch(); + return next_header != null; + } + + /* + * Return the next element. + */ + public Object nextElement() { + if (next_header == null) + next_header = nextMatch(); + + if (next_header == null) + throw new NoSuchElementException("No more headers"); + + InternetHeader h = next_header; + next_header = null; + if (want_line) + return h.line; + else + return new Header(h.getName(), h.getValue()); + } + + /* + * Return the next Header object according to the match + * criteria, or null if none left. + */ + private InternetHeader nextMatch() { + next: + while (e.hasNext()) { + InternetHeader h = e.next(); + + // skip "place holder" headers + if (h.line == null) + continue; + + // if no names to match against, return appropriately + if (names == null) + return match ? null : h; + + // check whether this header matches any of the names + for (int i = 0; i < names.length; i++) { + if (names[i].equalsIgnoreCase(h.getName())) { + if (match) + return h; + else + // found a match, but we're + // looking for non-matches. + // try next header. + continue next; + } + } + // found no matches. if that's what we wanted, return it. + if (!match) + return h; + } + return null; + } + } + + static class MatchStringEnum extends MatchEnum + implements Enumeration { + + MatchStringEnum(List v, String[] n, boolean m) { + super(v, n, m, true); + } + + @Override + public String nextElement() { + return (String) super.nextElement(); + } + + } + + static class MatchHeaderEnum extends MatchEnum + implements Enumeration

{ + + MatchHeaderEnum(List v, String[] n, boolean m) { + super(v, n, m, false); + } + + @Override + public Header nextElement() { + return (Header) super.nextElement(); + } + + } + + /** + * The actual list of Headers, including placeholder entries. + * Placeholder entries are Headers with a null value and + * are never seen by clients of the InternetHeaders class. + * Placeholder entries are used to keep track of the preferred + * order of headers. Headers are never actually removed from + * the list, they're converted into placeholder entries. + * New headers are added after existing headers of the same name + * (or before in the case of Received and + * Return-Path headers). If no existing header + * or placeholder for the header is found, new headers are + * added after the special placeholder with the name ":". + * + * @since JavaMail 1.4 + */ + protected List headers; + + /** + * Create an empty InternetHeaders object. Placeholder entries + * are inserted to indicate the preferred order of headers. + */ + public InternetHeaders() { + headers = new ArrayList<>(40); + headers.add(new InternetHeader("Return-Path", null)); + headers.add(new InternetHeader("Received", null)); + headers.add(new InternetHeader("Resent-Date", null)); + headers.add(new InternetHeader("Resent-From", null)); + headers.add(new InternetHeader("Resent-Sender", null)); + headers.add(new InternetHeader("Resent-To", null)); + headers.add(new InternetHeader("Resent-Cc", null)); + headers.add(new InternetHeader("Resent-Bcc", null)); + headers.add(new InternetHeader("Resent-Message-Id", null)); + headers.add(new InternetHeader("Date", null)); + headers.add(new InternetHeader("From", null)); + headers.add(new InternetHeader("Sender", null)); + headers.add(new InternetHeader("Reply-To", null)); + headers.add(new InternetHeader("To", null)); + headers.add(new InternetHeader("Cc", null)); + headers.add(new InternetHeader("Bcc", null)); + headers.add(new InternetHeader("Message-Id", null)); + headers.add(new InternetHeader("In-Reply-To", null)); + headers.add(new InternetHeader("References", null)); + headers.add(new InternetHeader("Subject", null)); + headers.add(new InternetHeader("Comments", null)); + headers.add(new InternetHeader("Keywords", null)); + headers.add(new InternetHeader("Errors-To", null)); + headers.add(new InternetHeader("MIME-Version", null)); + headers.add(new InternetHeader("Content-Type", null)); + headers.add(new InternetHeader("Content-Transfer-Encoding", null)); + headers.add(new InternetHeader("Content-MD5", null)); + headers.add(new InternetHeader(":", null)); + headers.add(new InternetHeader("Content-Length", null)); + headers.add(new InternetHeader("Status", null)); + } + + /** + * Read and parse the given RFC822 message stream till the + * blank line separating the header from the body. The input + * stream is left positioned at the start of the body. The + * header lines are stored internally.

+ * + * For efficiency, wrap a BufferedInputStream around the actual + * input stream and pass it as the parameter.

+ * + * No placeholder entries are inserted; the original order of + * the headers is preserved. + * + * @param is RFC822 input stream + * @exception MessagingException for any I/O error reading the stream + */ + public InternetHeaders(InputStream is) throws MessagingException { + this(is, false); + } + + /** + * Read and parse the given RFC822 message stream till the + * blank line separating the header from the body. The input + * stream is left positioned at the start of the body. The + * header lines are stored internally.

+ * + * For efficiency, wrap a BufferedInputStream around the actual + * input stream and pass it as the parameter.

+ * + * No placeholder entries are inserted; the original order of + * the headers is preserved. + * + * @param is RFC822 input stream + * @param allowutf8 if UTF-8 encoded headers are allowed + * @exception MessagingException for any I/O error reading the stream + * @since JavaMail 1.6 + */ + public InternetHeaders(InputStream is, boolean allowutf8) + throws MessagingException { + headers = new ArrayList<>(40); + load(is, allowutf8); + } + + /** + * Read and parse the given RFC822 message stream till the + * blank line separating the header from the body. Store the + * header lines inside this InternetHeaders object. The order + * of header lines is preserved.

+ * + * Note that the header lines are added into this InternetHeaders + * object, so any existing headers in this object will not be + * affected. Headers are added to the end of the existing list + * of headers, in order. + * + * @param is RFC822 input stream + * @exception MessagingException for any I/O error reading the stream + */ + public void load(InputStream is) throws MessagingException { + load(is, false); + } + + /** + * Read and parse the given RFC822 message stream till the + * blank line separating the header from the body. Store the + * header lines inside this InternetHeaders object. The order + * of header lines is preserved.

+ * + * Note that the header lines are added into this InternetHeaders + * object, so any existing headers in this object will not be + * affected. Headers are added to the end of the existing list + * of headers, in order. + * + * @param is RFC822 input stream + * @param allowutf8 if UTF-8 encoded headers are allowed + * @exception MessagingException for any I/O error reading the stream + * @since JavaMail 1.6 + */ + public void load(InputStream is, boolean allowutf8) + throws MessagingException { + // Read header lines until a blank line. It is valid + // to have BodyParts with no header lines. + String line; + LineInputStream lis = new LineInputStream(is, allowutf8); + String prevline = null; // the previous header line, as a string + // a buffer to accumulate the header in, when we know it's needed + StringBuilder lineBuffer = new StringBuilder(); + + try { + // if the first line being read is a continuation line, + // we ignore it if it's otherwise empty or we treat it as + // a non-continuation line if it has non-whitespace content + boolean first = true; + do { + line = lis.readLine(); + if (line != null && + (line.startsWith(" ") || line.startsWith("\t"))) { + // continuation of header + if (prevline != null) { + lineBuffer.append(prevline); + prevline = null; + } + if (first) { + String lt = line.trim(); + if (lt.length() > 0) + lineBuffer.append(lt); + } else { + if (lineBuffer.length() > 0) + lineBuffer.append("\r\n"); + lineBuffer.append(line); + } + } else { + // new header + if (prevline != null) + addHeaderLine(prevline); + else if (lineBuffer.length() > 0) { + // store previous header first + addHeaderLine(lineBuffer.toString()); + lineBuffer.setLength(0); + } + prevline = line; + } + first = false; + } while (line != null && !isEmpty(line)); + } catch (IOException ioex) { + throw new MessagingException("Error in input stream", ioex); + } + } + + /** + * Is this line an empty (blank) line? + */ + private static final boolean isEmpty(String line) { + return line.length() == 0 || + (ignoreWhitespaceLines && line.trim().length() == 0); + } + + /** + * Return all the values for the specified header. The + * values are String objects. Returns null + * if no headers with the specified name exist. + * + * @param name header name + * @return array of header values, or null if none + */ + public String[] getHeader(String name) { + Iterator e = headers.iterator(); + // XXX - should we just step through in index order? + List v = new ArrayList<>(); // accumulate return values + + while (e.hasNext()) { + InternetHeader h = e.next(); + if (name.equalsIgnoreCase(h.getName()) && h.line != null) { + v.add(h.getValue()); + } + } + if (v.size() == 0) + return (null); + // convert List to an array for return + String r[] = new String[v.size()]; + r = v.toArray(r); + return (r); + } + + /** + * Get all the headers for this header name, returned as a single + * String, with headers separated by the delimiter. If the + * delimiter is null, only the first header is + * returned. Returns null + * if no headers with the specified name exist. + * + * @param name header name + * @param delimiter delimiter + * @return the value fields for all headers with + * this name, or null if none + */ + public String getHeader(String name, String delimiter) { + String s[] = getHeader(name); + + if (s == null) + return null; + + if ((s.length == 1) || delimiter == null) + return s[0]; + + StringBuilder r = new StringBuilder(s[0]); + for (int i = 1; i < s.length; i++) { + r.append(delimiter); + r.append(s[i]); + } + return r.toString(); + } + + /** + * Change the first header line that matches name + * to have value, adding a new header if no existing header + * matches. Remove all matching headers but the first.

+ * + * Note that RFC822 headers can only contain US-ASCII characters + * + * @param name header name + * @param value header value + */ + public void setHeader(String name, String value) { + boolean found = false; + + for (int i = 0; i < headers.size(); i++) { + InternetHeader h = headers.get(i); + if (name.equalsIgnoreCase(h.getName())) { + if (!found) { + int j; + if (h.line != null && (j = h.line.indexOf(':')) >= 0) { + h.line = h.line.substring(0, j + 1) + " " + value; + // preserves capitalization, spacing + } else { + h.line = name + ": " + value; + } + found = true; + } else { + headers.remove(i); + i--; // have to look at i again + } + } + } + + if (!found) { + addHeader(name, value); + } + } + + /** + * Add a header with the specified name and value to the header list.

+ * + * The current implementation knows about the preferred order of most + * well-known headers and will insert headers in that order. In + * addition, it knows that Received headers should be + * inserted in reverse order (newest before oldest), and that they + * should appear at the beginning of the headers, preceeded only by + * a possible Return-Path header.

+ * + * Note that RFC822 headers can only contain US-ASCII characters. + * + * @param name header name + * @param value header value + */ + public void addHeader(String name, String value) { + int pos = headers.size(); + boolean addReverse = + name.equalsIgnoreCase("Received") || + name.equalsIgnoreCase("Return-Path"); + if (addReverse) + pos = 0; + for (int i = headers.size() - 1; i >= 0; i--) { + InternetHeader h = headers.get(i); + if (name.equalsIgnoreCase(h.getName())) { + if (addReverse) { + pos = i; + } else { + headers.add(i + 1, new InternetHeader(name, value)); + return; + } + } + // marker for default place to add new headers + if (!addReverse && h.getName().equals(":")) + pos = i; + } + headers.add(pos, new InternetHeader(name, value)); + } + + /** + * Remove all header entries that match the given name + * @param name header name + */ + public void removeHeader(String name) { + for (int i = 0; i < headers.size(); i++) { + InternetHeader h = headers.get(i); + if (name.equalsIgnoreCase(h.getName())) { + h.line = null; + //headers.remove(i); + //i--; // have to look at i again + } + } + } + + /** + * Return all the headers as an Enumeration of + * {@link javax.mail.Header} objects. + * + * @return Enumeration of Header objects + */ + public Enumeration

getAllHeaders() { + return (new MatchHeaderEnum(headers, null, false)); + } + + /** + * Return all matching {@link javax.mail.Header} objects. + * + * @param names the headers to return + * @return Enumeration of matching Header objects + */ + public Enumeration
getMatchingHeaders(String[] names) { + return (new MatchHeaderEnum(headers, names, true)); + } + + /** + * Return all non-matching {@link javax.mail.Header} objects. + * + * @param names the headers to not return + * @return Enumeration of non-matching Header objects + */ + public Enumeration
getNonMatchingHeaders(String[] names) { + return (new MatchHeaderEnum(headers, names, false)); + } + + /** + * Add an RFC822 header line to the header store. + * If the line starts with a space or tab (a continuation line), + * add it to the last header line in the list. Otherwise, + * append the new header line to the list.

+ * + * Note that RFC822 headers can only contain US-ASCII characters + * + * @param line raw RFC822 header line + */ + public void addHeaderLine(String line) { + try { + char c = line.charAt(0); + if (c == ' ' || c == '\t') { + InternetHeader h = headers.get(headers.size() - 1); + h.line += "\r\n" + line; + } else + headers.add(new InternetHeader(line)); + } catch (StringIndexOutOfBoundsException e) { + // line is empty, ignore it + return; + } catch (NoSuchElementException e) { + // XXX - list is empty? + } + } + + /** + * Return all the header lines as an Enumeration of Strings. + * + * @return Enumeration of Strings of all header lines + */ + public Enumeration getAllHeaderLines() { + return (getNonMatchingHeaderLines(null)); + } + + /** + * Return all matching header lines as an Enumeration of Strings. + * + * @param names the headers to return + * @return Enumeration of Strings of all matching header lines + */ + public Enumeration getMatchingHeaderLines(String[] names) { + return (new MatchStringEnum(headers, names, true)); + } + + /** + * Return all non-matching header lines + * + * @param names the headers to not return + * @return Enumeration of Strings of all non-matching header lines + */ + public Enumeration getNonMatchingHeaderLines(String[] names) { + return (new MatchStringEnum(headers, names, false)); + } +} diff --git a/app/src/main/java/javax/mail/internet/MailDateFormat.java b/app/src/main/java/javax/mail/internet/MailDateFormat.java new file mode 100644 index 0000000000..b82521b1bf --- /dev/null +++ b/app/src/main/java/javax/mail/internet/MailDateFormat.java @@ -0,0 +1,1042 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectStreamException; +import java.util.Date; +import java.util.Calendar; +import java.util.Locale; +import java.util.TimeZone; +import java.util.logging.Level; +import java.text.DateFormatSymbols; +import java.text.SimpleDateFormat; +import java.text.NumberFormat; +import java.text.FieldPosition; +import java.text.ParsePosition; +import java.text.ParseException; + +import com.sun.mail.util.MailLogger; + +/** + * Formats and parses date specification based on + * RFC 2822.

+ * + * This class does not support methods that influence the format. It always + * formats the date based on the specification below.

+ * + * 3.3. Date and Time Specification + *

+ * Date and time occur in several header fields. This section specifies + * the syntax for a full date and time specification. Though folding + * white space is permitted throughout the date-time specification, it is + * RECOMMENDED that a single space be used in each place that FWS appears + * (whether it is required or optional); some older implementations may + * not interpret other occurrences of folding white space correctly. + *

+ * date-time       =       [ day-of-week "," ] date FWS time [CFWS]
+ *
+ * day-of-week     =       ([FWS] day-name) / obs-day-of-week
+ *
+ * day-name        =       "Mon" / "Tue" / "Wed" / "Thu" /
+ *                         "Fri" / "Sat" / "Sun"
+ *
+ * date            =       day month year
+ *
+ * year            =       4*DIGIT / obs-year
+ *
+ * month           =       (FWS month-name FWS) / obs-month
+ *
+ * month-name      =       "Jan" / "Feb" / "Mar" / "Apr" /
+ *                         "May" / "Jun" / "Jul" / "Aug" /
+ *                         "Sep" / "Oct" / "Nov" / "Dec"
+ *
+ * day             =       ([FWS] 1*2DIGIT) / obs-day
+ *
+ * time            =       time-of-day FWS zone
+ *
+ * time-of-day     =       hour ":" minute [ ":" second ]
+ *
+ * hour            =       2DIGIT / obs-hour
+ *
+ * minute          =       2DIGIT / obs-minute
+ *
+ * second          =       2DIGIT / obs-second
+ *
+ * zone            =       (( "+" / "-" ) 4DIGIT) / obs-zone
+ * 
+ * The day is the numeric day of the month. The year is any numeric year + * 1900 or later. + *

+ * The time-of-day specifies the number of hours, minutes, and optionally + * seconds since midnight of the date indicated. + *

+ * The date and time-of-day SHOULD express local time. + *

+ * The zone specifies the offset from Coordinated Universal Time (UTC, + * formerly referred to as "Greenwich Mean Time") that the date and + * time-of-day represent. The "+" or "-" indicates whether the + * time-of-day is ahead of (i.e., east of) or behind (i.e., west of) + * Universal Time. The first two digits indicate the number of hours + * difference from Universal Time, and the last two digits indicate the + * number of minutes difference from Universal Time. (Hence, +hhmm means + * +(hh * 60 + mm) minutes, and -hhmm means -(hh * 60 + mm) minutes). The + * form "+0000" SHOULD be used to indicate a time zone at Universal Time. + * Though "-0000" also indicates Universal Time, it is used to indicate + * that the time was generated on a system that may be in a local time + * zone other than Universal Time and therefore indicates that the + * date-time contains no information about the local time zone. + *

+ * A date-time specification MUST be semantically valid. That is, the + * day-of-the-week (if included) MUST be the day implied by the date, the + * numeric day-of-month MUST be between 1 and the number of days allowed + * for the specified month (in the specified year), the time-of-day MUST + * be in the range 00:00:00 through 23:59:60 (the number of seconds + * allowing for a leap second; see [STD12]), and the zone MUST be within + * the range -9959 through +9959. + * + *

Synchronization

+ * + *

+ * Date formats are not synchronized. + * It is recommended to create separate format instances for each thread. + * If multiple threads access a format concurrently, it must be synchronized + * externally. + * + * @author Anthony Vanelverdinghe + * @author Max Spivak + * @since JavaMail 1.2 + */ +public class MailDateFormat extends SimpleDateFormat { + + private static final long serialVersionUID = -8148227605210628779L; + private static final String PATTERN = "EEE, d MMM yyyy HH:mm:ss Z (z)"; + + private static final MailLogger LOGGER = new MailLogger( + MailDateFormat.class, "DEBUG", false, System.out); + + private static final int UNKNOWN_DAY_NAME = -1; + private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + private static final int LEAP_SECOND = 60; + + /** + * Create a new date format for the RFC2822 specification with lenient + * parsing. + */ + public MailDateFormat() { + super(PATTERN, Locale.US); + } + + /** + * Allows to serialize instances such that they are deserializable with the + * previous implementation. + * + * @return the object to be serialized + * @throws ObjectStreamException never + */ + private Object writeReplace() throws ObjectStreamException { + MailDateFormat fmt = new MailDateFormat(); + fmt.superApplyPattern("EEE, d MMM yyyy HH:mm:ss 'XXXXX' (z)"); + fmt.setTimeZone(getTimeZone()); + return fmt; + } + + /** + * Allows to deserialize instances that were serialized with the previous + * implementation. + * + * @param in the stream containing the serialized object + * @throws IOException on read failures + * @throws ClassNotFoundException never + */ + private void readObject(ObjectInputStream in) + throws IOException, ClassNotFoundException { + in.defaultReadObject(); + super.applyPattern(PATTERN); + } + + /** + * Overrides Cloneable. + * + * @return a clone of this instance + * @since JavaMail 1.6 + */ + @Override + public MailDateFormat clone() { + return (MailDateFormat) super.clone(); + } + + /** + * Formats the given date in the format specified by + * RFC 2822 in the current TimeZone. + * + * @param date the Date object + * @param dateStrBuf the formatted string + * @param fieldPosition the current field position + * @return StringBuffer the formatted String + * @since JavaMail 1.2 + */ + @Override + public StringBuffer format(Date date, StringBuffer dateStrBuf, + FieldPosition fieldPosition) { + return super.format(date, dateStrBuf, fieldPosition); + } + + /** + * Parses the given date in the format specified by + * RFC 2822. + *

    + *
  • With strict parsing, obs-* tokens are unsupported. Lenient parsing + * supports obs-year and obs-zone, with the exception of the 1-character + * military time zones. + *
  • The optional CFWS token at the end is not parsed. + *
  • RFC 2822 specifies that a zone of "-0000" indicates that the + * date-time contains no information about the local time zone. This class + * uses the UTC time zone in this case. + *
+ * + * @param text the formatted date to be parsed + * @param pos the current parse position + * @return Date the parsed date. In case of error, returns null. + * @since JavaMail 1.2 + */ + @Override + public Date parse(String text, ParsePosition pos) { + if (text == null || pos == null) { + throw new NullPointerException(); + } else if (0 > pos.getIndex() || pos.getIndex() >= text.length()) { + return null; + } + + return isLenient() + ? new Rfc2822LenientParser(text, pos).parse() + : new Rfc2822StrictParser(text, pos).parse(); + } + + /** + * This method always throws an UnsupportedOperationException and should not + * be used because RFC 2822 mandates a specific calendar. + * + * @throws UnsupportedOperationException if this method is invoked + */ + @Override + public void setCalendar(Calendar newCalendar) { + throw new UnsupportedOperationException("Method " + + "setCalendar() shouldn't be called"); + } + + /** + * This method always throws an UnsupportedOperationException and should not + * be used because RFC 2822 mandates a specific number format. + * + * @throws UnsupportedOperationException if this method is invoked + */ + @Override + public void setNumberFormat(NumberFormat newNumberFormat) { + throw new UnsupportedOperationException("Method " + + "setNumberFormat() shouldn't be called"); + } + + /** + * This method always throws an UnsupportedOperationException and should not + * be used because RFC 2822 mandates a specific pattern. + * + * @throws UnsupportedOperationException if this method is invoked + * @since JavaMail 1.6 + */ + @Override + public void applyLocalizedPattern(String pattern) { + throw new UnsupportedOperationException("Method " + + "applyLocalizedPattern() shouldn't be called"); + } + + /** + * This method always throws an UnsupportedOperationException and should not + * be used because RFC 2822 mandates a specific pattern. + * + * @throws UnsupportedOperationException if this method is invoked + * @since JavaMail 1.6 + */ + @Override + public void applyPattern(String pattern) { + throw new UnsupportedOperationException("Method " + + "applyPattern() shouldn't be called"); + } + + /** + * This method allows serialization to change the pattern. + */ + private void superApplyPattern(String pattern) { + super.applyPattern(pattern); + } + + /** + * This method always throws an UnsupportedOperationException and should not + * be used because RFC 2822 mandates another strategy for interpreting + * 2-digits years. + * + * @return the start of the 100-year period into which two digit years are + * parsed + * @throws UnsupportedOperationException if this method is invoked + * @since JavaMail 1.6 + */ + @Override + public Date get2DigitYearStart() { + throw new UnsupportedOperationException("Method " + + "get2DigitYearStart() shouldn't be called"); + } + + /** + * This method always throws an UnsupportedOperationException and should not + * be used because RFC 2822 mandates another strategy for interpreting + * 2-digits years. + * + * @throws UnsupportedOperationException if this method is invoked + * @since JavaMail 1.6 + */ + @Override + public void set2DigitYearStart(Date startDate) { + throw new UnsupportedOperationException("Method " + + "set2DigitYearStart() shouldn't be called"); + } + + /** + * This method always throws an UnsupportedOperationException and should not + * be used because RFC 2822 mandates specific date format symbols. + * + * @throws UnsupportedOperationException if this method is invoked + * @since JavaMail 1.6 + */ + @Override + public void setDateFormatSymbols(DateFormatSymbols newFormatSymbols) { + throw new UnsupportedOperationException("Method " + + "setDateFormatSymbols() shouldn't be called"); + } + + /** + * Returns the date, as specified by the parameters. + * + * @param dayName + * @param day + * @param month + * @param year + * @param hour + * @param minute + * @param second + * @param zone + * @return the date, as specified by the parameters + * @throws IllegalArgumentException if this instance's Calendar is + * non-lenient and any of the parameters have invalid values, or if dayName + * is not consistent with day-month-year + */ + private Date toDate(int dayName, int day, int month, int year, + int hour, int minute, int second, int zone) { + if (second == LEAP_SECOND) { + second = 59; + } + + TimeZone tz = calendar.getTimeZone(); + try { + calendar.setTimeZone(UTC); + calendar.clear(); + calendar.set(year, month, day, hour, minute, second); + + if (dayName == UNKNOWN_DAY_NAME + || dayName == calendar.get(Calendar.DAY_OF_WEEK)) { + calendar.add(Calendar.MINUTE, zone); + return calendar.getTime(); + } else { + throw new IllegalArgumentException("Inconsistent day-name"); + } + } finally { + calendar.setTimeZone(tz); + } + } + + /** + * This class provides the building blocks for date parsing. + *

+ * It has the following invariants: + *

    + *
  • no exceptions are thrown, except for java.text.ParseException from + * parse* methods + *
  • when parse* throws ParseException OR get* returns INVALID_CHAR OR + * skip* returns false OR peek* is invoked, then pos.getIndex() on method + * exit is the same as it was on method entry + *
+ */ + private static abstract class AbstractDateParser { + + static final int INVALID_CHAR = -1; + static final int MAX_YEAR_DIGITS = 8; // guarantees that: + // year < new GregorianCalendar().getMaximum(Calendar.YEAR) + + final String text; + final ParsePosition pos; + + AbstractDateParser(String text, ParsePosition pos) { + this.text = text; + this.pos = pos; + } + + final Date parse() { + int startPosition = pos.getIndex(); + try { + return tryParse(); + } catch (Exception e) { // == ParseException | RuntimeException e + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, "Bad date: '" + text + "'", e); + } + pos.setErrorIndex(pos.getIndex()); + pos.setIndex(startPosition); + return null; + } + } + + abstract Date tryParse() throws ParseException; + + /** + * @return the java.util.Calendar constant for the parsed day name + */ + final int parseDayName() throws ParseException { + switch (getChar()) { + case 'S': + if (skipPair('u', 'n')) { + return Calendar.SUNDAY; + } else if (skipPair('a', 't')) { + return Calendar.SATURDAY; + } + break; + case 'T': + if (skipPair('u', 'e')) { + return Calendar.TUESDAY; + } else if (skipPair('h', 'u')) { + return Calendar.THURSDAY; + } + break; + case 'M': + if (skipPair('o', 'n')) { + return Calendar.MONDAY; + } + break; + case 'W': + if (skipPair('e', 'd')) { + return Calendar.WEDNESDAY; + } + break; + case 'F': + if (skipPair('r', 'i')) { + return Calendar.FRIDAY; + } + break; + case INVALID_CHAR: + throw new ParseException("Invalid day-name", + pos.getIndex()); + } + pos.setIndex(pos.getIndex() - 1); + throw new ParseException("Invalid day-name", pos.getIndex()); + } + + /** + * @return the java.util.Calendar constant for the parsed month name + */ + @SuppressWarnings("fallthrough") + final int parseMonthName(boolean caseSensitive) throws ParseException { + switch (getChar()) { + case 'j': + if (caseSensitive) { + break; + } + case 'J': + if (skipChar('u') || (!caseSensitive && skipChar('U'))) { + if (skipChar('l') || (!caseSensitive + && skipChar('L'))) { + return Calendar.JULY; + } else if (skipChar('n') || (!caseSensitive + && skipChar('N'))) { + return Calendar.JUNE; + } else { + pos.setIndex(pos.getIndex() - 1); + } + } else if (skipPair('a', 'n') || (!caseSensitive + && skipAlternativePair('a', 'A', 'n', 'N'))) { + return Calendar.JANUARY; + } + break; + case 'm': + if (caseSensitive) { + break; + } + case 'M': + if (skipChar('a') || (!caseSensitive && skipChar('A'))) { + if (skipChar('r') || (!caseSensitive + && skipChar('R'))) { + return Calendar.MARCH; + } else if (skipChar('y') || (!caseSensitive + && skipChar('Y'))) { + return Calendar.MAY; + } else { + pos.setIndex(pos.getIndex() - 1); + } + } + break; + case 'a': + if (caseSensitive) { + break; + } + case 'A': + if (skipPair('u', 'g') || (!caseSensitive + && skipAlternativePair('u', 'U', 'g', 'G'))) { + return Calendar.AUGUST; + } else if (skipPair('p', 'r') || (!caseSensitive + && skipAlternativePair('p', 'P', 'r', 'R'))) { + return Calendar.APRIL; + } + break; + case 'd': + if (caseSensitive) { + break; + } + case 'D': + if (skipPair('e', 'c') || (!caseSensitive + && skipAlternativePair('e', 'E', 'c', 'C'))) { + return Calendar.DECEMBER; + } + break; + case 'o': + if (caseSensitive) { + break; + } + case 'O': + if (skipPair('c', 't') || (!caseSensitive + && skipAlternativePair('c', 'C', 't', 'T'))) { + return Calendar.OCTOBER; + } + break; + case 's': + if (caseSensitive) { + break; + } + case 'S': + if (skipPair('e', 'p') || (!caseSensitive + && skipAlternativePair('e', 'E', 'p', 'P'))) { + return Calendar.SEPTEMBER; + } + break; + case 'n': + if (caseSensitive) { + break; + } + case 'N': + if (skipPair('o', 'v') || (!caseSensitive + && skipAlternativePair('o', 'O', 'v', 'V'))) { + return Calendar.NOVEMBER; + } + break; + case 'f': + if (caseSensitive) { + break; + } + case 'F': + if (skipPair('e', 'b') || (!caseSensitive + && skipAlternativePair('e', 'E', 'b', 'B'))) { + return Calendar.FEBRUARY; + } + break; + case INVALID_CHAR: + throw new ParseException("Invalid month", pos.getIndex()); + } + pos.setIndex(pos.getIndex() - 1); + throw new ParseException("Invalid month", pos.getIndex()); + } + + /** + * @return the number of minutes to be added to the time in the local + * time zone, in order to obtain the equivalent time in the UTC time + * zone. Returns 0 if the date-time contains no information about the + * local time zone. + */ + final int parseZoneOffset() throws ParseException { + int sign = getChar(); + if (sign == '+' || sign == '-') { + int offset = parseAsciiDigits(4, 4, true); + if (!isValidZoneOffset(offset)) { + pos.setIndex(pos.getIndex() - 5); + throw new ParseException("Invalid zone", pos.getIndex()); + } + + return ((sign == '+') ? -1 : 1) + * (offset / 100 * 60 + offset % 100); + } else if (sign != INVALID_CHAR) { + pos.setIndex(pos.getIndex() - 1); + } + throw new ParseException("Invalid zone", pos.getIndex()); + } + + boolean isValidZoneOffset(int offset) { + return (offset % 100) < 60; + } + + final int parseAsciiDigits(int count) throws ParseException { + return parseAsciiDigits(count, count); + } + + final int parseAsciiDigits(int min, int max) throws ParseException { + return parseAsciiDigits(min, max, false); + } + + final int parseAsciiDigits(int min, int max, boolean isEOF) + throws ParseException { + int result = 0; + int nbDigitsParsed = 0; + while (nbDigitsParsed < max && peekAsciiDigit()) { + result = result * 10 + getAsciiDigit(); + nbDigitsParsed++; + } + + if ((nbDigitsParsed < min) + || (nbDigitsParsed == max && !isEOF && peekAsciiDigit())) { + pos.setIndex(pos.getIndex() - nbDigitsParsed); + } else { + return result; + } + + String range = (min == max) + ? Integer.toString(min) + : "between " + min + " and " + max; + throw new ParseException("Invalid input: expected " + + range + " ASCII digits", pos.getIndex()); + } + + final void parseFoldingWhiteSpace() throws ParseException { + if (!skipFoldingWhiteSpace()) { + throw new ParseException("Invalid input: expected FWS", + pos.getIndex()); + } + } + + final void parseChar(char ch) throws ParseException { + if (!skipChar(ch)) { + throw new ParseException("Invalid input: expected '" + ch + "'", + pos.getIndex()); + } + } + + final int getAsciiDigit() { + int ch = getChar(); + if ('0' <= ch && ch <= '9') { + return Character.digit((char) ch, 10); + } else { + if (ch != INVALID_CHAR) { + pos.setIndex(pos.getIndex() - 1); + } + return INVALID_CHAR; + } + } + + final int getChar() { + if (pos.getIndex() < text.length()) { + char ch = text.charAt(pos.getIndex()); + pos.setIndex(pos.getIndex() + 1); + return ch; + } else { + return INVALID_CHAR; + } + } + + boolean skipFoldingWhiteSpace() { + // fast paths: a single ASCII space or no FWS + if (skipChar(' ')) { + if (!peekFoldingWhiteSpace()) { + return true; + } else { + pos.setIndex(pos.getIndex() - 1); + } + } else if (!peekFoldingWhiteSpace()) { + return false; + } + + // normal path + int startIndex = pos.getIndex(); + if (skipWhiteSpace()) { + while (skipNewline()) { + if (!skipWhiteSpace()) { + pos.setIndex(startIndex); + return false; + } + } + return true; + } else if (skipNewline() && skipWhiteSpace()) { + return true; + } else { + pos.setIndex(startIndex); + return false; + } + } + + final boolean skipWhiteSpace() { + int startIndex = pos.getIndex(); + while (skipAlternative(' ', '\t')) { /* empty */ } + return pos.getIndex() > startIndex; + } + + final boolean skipNewline() { + return skipPair('\r', '\n'); + } + + final boolean skipAlternativeTriple( + char firstStandard, char firstAlternative, + char secondStandard, char secondAlternative, + char thirdStandard, char thirdAlternative + ) { + if (skipAlternativePair(firstStandard, firstAlternative, + secondStandard, secondAlternative)) { + if (skipAlternative(thirdStandard, thirdAlternative)) { + return true; + } else { + pos.setIndex(pos.getIndex() - 2); + } + } + return false; + } + + final boolean skipAlternativePair( + char firstStandard, char firstAlternative, + char secondStandard, char secondAlternative + ) { + if (skipAlternative(firstStandard, firstAlternative)) { + if (skipAlternative(secondStandard, secondAlternative)) { + return true; + } else { + pos.setIndex(pos.getIndex() - 1); + } + } + return false; + } + + final boolean skipAlternative(char standard, char alternative) { + return skipChar(standard) || skipChar(alternative); + } + + final boolean skipPair(char first, char second) { + if (skipChar(first)) { + if (skipChar(second)) { + return true; + } else { + pos.setIndex(pos.getIndex() - 1); + } + } + return false; + } + + final boolean skipChar(char ch) { + if (pos.getIndex() < text.length() + && text.charAt(pos.getIndex()) == ch) { + pos.setIndex(pos.getIndex() + 1); + return true; + } else { + return false; + } + } + + final boolean peekAsciiDigit() { + return (pos.getIndex() < text.length() + && '0' <= text.charAt(pos.getIndex()) + && text.charAt(pos.getIndex()) <= '9'); + } + + boolean peekFoldingWhiteSpace() { + return (pos.getIndex() < text.length() + && (text.charAt(pos.getIndex()) == ' ' + || text.charAt(pos.getIndex()) == '\t' + || text.charAt(pos.getIndex()) == '\r')); + } + + final boolean peekChar(char ch) { + return (pos.getIndex() < text.length() + && text.charAt(pos.getIndex()) == ch); + } + + } + + private class Rfc2822StrictParser extends AbstractDateParser { + + Rfc2822StrictParser(String text, ParsePosition pos) { + super(text, pos); + } + + @Override + Date tryParse() throws ParseException { + int dayName = parseOptionalBegin(); + + int day = parseDay(); + int month = parseMonth(); + int year = parseYear(); + + parseFoldingWhiteSpace(); + + int hour = parseHour(); + parseChar(':'); + int minute = parseMinute(); + int second = (skipChar(':')) ? parseSecond() : 0; + + parseFwsBetweenTimeOfDayAndZone(); + + int zone = parseZone(); + + try { + return MailDateFormat.this.toDate(dayName, day, month, year, + hour, minute, second, zone); + } catch (IllegalArgumentException e) { + throw new ParseException("Invalid input: some of the calendar " + + "fields have invalid values, or day-name is " + + "inconsistent with date", pos.getIndex()); + } + } + + /** + * @return the java.util.Calendar constant for the parsed day name, or + * UNKNOWN_DAY_NAME iff the begin is missing + */ + int parseOptionalBegin() throws ParseException { + int dayName; + if (!peekAsciiDigit()) { + skipFoldingWhiteSpace(); + dayName = parseDayName(); + parseChar(','); + } else { + dayName = UNKNOWN_DAY_NAME; + } + return dayName; + } + + int parseDay() throws ParseException { + skipFoldingWhiteSpace(); + return parseAsciiDigits(1, 2); + } + + /** + * @return the java.util.Calendar constant for the parsed month name + */ + int parseMonth() throws ParseException { + parseFwsInMonth(); + int month = parseMonthName(isMonthNameCaseSensitive()); + parseFwsInMonth(); + return month; + } + + void parseFwsInMonth() throws ParseException { + parseFoldingWhiteSpace(); + } + + boolean isMonthNameCaseSensitive() { + return true; + } + + int parseYear() throws ParseException { + int year = parseAsciiDigits(4, MAX_YEAR_DIGITS); + if (year >= 1900) { + return year; + } else { + pos.setIndex(pos.getIndex() - 4); + while (text.charAt(pos.getIndex() - 1) == '0') { + pos.setIndex(pos.getIndex() - 1); + } + throw new ParseException("Invalid year", pos.getIndex()); + } + } + + int parseHour() throws ParseException { + return parseAsciiDigits(2); + } + + int parseMinute() throws ParseException { + return parseAsciiDigits(2); + } + + int parseSecond() throws ParseException { + return parseAsciiDigits(2); + } + + void parseFwsBetweenTimeOfDayAndZone() throws ParseException { + parseFoldingWhiteSpace(); + } + + int parseZone() throws ParseException { + return parseZoneOffset(); + } + + } + + private class Rfc2822LenientParser extends Rfc2822StrictParser { + + private Boolean hasDefaultFws; + + Rfc2822LenientParser(String text, ParsePosition pos) { + super(text, pos); + } + + @Override + int parseOptionalBegin() { + while (pos.getIndex() < text.length() && !peekAsciiDigit()) { + pos.setIndex(pos.getIndex() + 1); + } + + return UNKNOWN_DAY_NAME; + } + + @Override + int parseDay() throws ParseException { + skipFoldingWhiteSpace(); + return parseAsciiDigits(1, 3); + } + + @Override + void parseFwsInMonth() throws ParseException { + // '-' is allowed to accomodate for the date format as specified in + // RFC 3501 + if (hasDefaultFws == null) { + hasDefaultFws = !skipChar('-'); + skipFoldingWhiteSpace(); + } else if (hasDefaultFws) { + skipFoldingWhiteSpace(); + } else { + parseChar('-'); + } + } + + @Override + boolean isMonthNameCaseSensitive() { + return false; + } + + @Override + int parseYear() throws ParseException { + int year = parseAsciiDigits(1, MAX_YEAR_DIGITS); + if (year >= 1000) { + return year; + } else if (year >= 50) { + return year + 1900; + } else { + return year + 2000; + } + } + + @Override + int parseHour() throws ParseException { + return parseAsciiDigits(1, 2); + } + + @Override + int parseMinute() throws ParseException { + return parseAsciiDigits(1, 2); + } + + @Override + int parseSecond() throws ParseException { + return parseAsciiDigits(1, 2); + } + + @Override + void parseFwsBetweenTimeOfDayAndZone() throws ParseException { + skipFoldingWhiteSpace(); + } + + @Override + int parseZone() throws ParseException { + try { + if (pos.getIndex() >= text.length()) { + throw new ParseException("Missing zone", pos.getIndex()); + } + + if (peekChar('+') || peekChar('-')) { + return parseZoneOffset(); + } else if (skipAlternativePair('U', 'u', 'T', 't')) { + return 0; + } else if (skipAlternativeTriple('G', 'g', 'M', 'm', + 'T', 't')) { + return 0; + } else { + int hoursOffset; + if (skipAlternative('E', 'e')) { + hoursOffset = 4; + } else if (skipAlternative('C', 'c')) { + hoursOffset = 5; + } else if (skipAlternative('M', 'm')) { + hoursOffset = 6; + } else if (skipAlternative('P', 'p')) { + hoursOffset = 7; + } else { + throw new ParseException("Invalid zone", + pos.getIndex()); + } + if (skipAlternativePair('S', 's', 'T', 't')) { + hoursOffset += 1; + } else if (skipAlternativePair('D', 'd', 'T', 't')) { + } else { + pos.setIndex(pos.getIndex() - 1); + throw new ParseException("Invalid zone", + pos.getIndex()); + } + return hoursOffset * 60; + } + } catch (ParseException e) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.log(Level.FINE, "No timezone? : '" + text + "'", e); + } + + return 0; + } + } + + @Override + boolean isValidZoneOffset(int offset) { + return true; + } + + @Override + boolean skipFoldingWhiteSpace() { + boolean result = peekFoldingWhiteSpace(); + + skipLoop: + while (pos.getIndex() < text.length()) { + switch (text.charAt(pos.getIndex())) { + case ' ': + case '\t': + case '\r': + case '\n': + pos.setIndex(pos.getIndex() + 1); + break; + default: + break skipLoop; + } + } + + return result; + } + + @Override + boolean peekFoldingWhiteSpace() { + return super.peekFoldingWhiteSpace() + || (pos.getIndex() < text.length() + && text.charAt(pos.getIndex()) == '\n'); + } + + } + +} diff --git a/app/src/main/java/javax/mail/internet/MimeBodyPart.java b/app/src/main/java/javax/mail/internet/MimeBodyPart.java new file mode 100644 index 0000000000..c1b44a9553 --- /dev/null +++ b/app/src/main/java/javax/mail/internet/MimeBodyPart.java @@ -0,0 +1,1712 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import javax.mail.*; +import javax.activation.*; +import java.io.*; +import java.util.*; +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.util.MimeUtil; +import com.sun.mail.util.MessageRemovedIOException; +import com.sun.mail.util.FolderClosedIOException; +import com.sun.mail.util.LineOutputStream; + +/** + * This class represents a MIME body part. It implements the + * BodyPart abstract class and the MimePart + * interface. MimeBodyParts are contained in MimeMultipart + * objects.

+ * + * MimeBodyPart uses the InternetHeaders class to parse + * and store the headers of that body part. + * + *


A note on RFC 822 and MIME headers

+ * + * RFC 822 header fields must contain only + * US-ASCII characters. MIME allows non ASCII characters to be present + * in certain portions of certain headers, by encoding those characters. + * RFC 2047 specifies the rules for doing this. The MimeUtility + * class provided in this package can be used to to achieve this. + * Callers of the setHeader, addHeader, and + * addHeaderLine methods are responsible for enforcing + * the MIME requirements for the specified headers. In addition, these + * header fields must be folded (wrapped) before being sent if they + * exceed the line length limitation for the transport (1000 bytes for + * SMTP). Received headers may have been folded. The application is + * responsible for folding and unfolding headers as appropriate.

+ * + * @author John Mani + * @author Bill Shannon + * @author Kanwar Oberoi + * @see javax.mail.Part + * @see javax.mail.internet.MimePart + * @see javax.mail.internet.MimeUtility + */ + +public class MimeBodyPart extends BodyPart implements MimePart { + + // Paranoia: + // allow this last minute change to be disabled if it causes problems + private static final boolean setDefaultTextCharset = + PropUtil.getBooleanSystemProperty( + "mail.mime.setdefaulttextcharset", true); + + private static final boolean setContentTypeFileName = + PropUtil.getBooleanSystemProperty( + "mail.mime.setcontenttypefilename", true); + + private static final boolean encodeFileName = + PropUtil.getBooleanSystemProperty("mail.mime.encodefilename", false); + private static final boolean decodeFileName = + PropUtil.getBooleanSystemProperty("mail.mime.decodefilename", false); + private static final boolean ignoreMultipartEncoding = + PropUtil.getBooleanSystemProperty( + "mail.mime.ignoremultipartencoding", true); + private static final boolean allowutf8 = + PropUtil.getBooleanSystemProperty("mail.mime.allowutf8", true); + + // Paranoia: + // allow this last minute change to be disabled if it causes problems + static final boolean cacheMultipart = // accessed by MimeMessage + PropUtil.getBooleanSystemProperty("mail.mime.cachemultipart", true); + + + /** + * The DataHandler object representing this Part's content. + */ + protected DataHandler dh; + + /** + * Byte array that holds the bytes of the content of this Part. + */ + protected byte[] content; + + /** + * If the data for this body part was supplied by an + * InputStream that implements the SharedInputStream interface, + * contentStream is another such stream representing + * the content of this body part. In this case, content + * will be null. + * + * @since JavaMail 1.2 + */ + protected InputStream contentStream; + + /** + * The InternetHeaders object that stores all the headers + * of this body part. + */ + protected InternetHeaders headers; + + /** + * If our content is a Multipart of Message object, we save it + * the first time it's created by parsing a stream so that changes + * to the contained objects will not be lost. + * + * If this field is not null, it's return by the {@link #getContent} + * method. The {@link #getContent} method sets this field if it + * would return a Multipart or MimeMessage object. This field is + * is cleared by the {@link #setDataHandler} method. + * + * @since JavaMail 1.5 + */ + protected Object cachedContent; + + /** + * An empty MimeBodyPart object is created. + * This body part maybe filled in by a client constructing a multipart + * message. + */ + public MimeBodyPart() { + super(); + headers = new InternetHeaders(); + } + + /** + * Constructs a MimeBodyPart by reading and parsing the data from + * the specified input stream. The parser consumes data till the end + * of the given input stream. The input stream must start at the + * beginning of a valid MIME body part and must terminate at the end + * of that body part.

+ * + * Note that the "boundary" string that delimits body parts must + * not be included in the input stream. The intention + * is that the MimeMultipart parser will extract each body part's bytes + * from a multipart stream and feed them into this constructor, without + * the delimiter strings. + * + * @param is the body part Input Stream + * @exception MessagingException for failures + */ + public MimeBodyPart(InputStream is) throws MessagingException { + if (!(is instanceof ByteArrayInputStream) && + !(is instanceof BufferedInputStream) && + !(is instanceof SharedInputStream)) + is = new BufferedInputStream(is); + + headers = new InternetHeaders(is); + + if (is instanceof SharedInputStream) { + SharedInputStream sis = (SharedInputStream)is; + contentStream = sis.newStream(sis.getPosition(), -1); + } else { + try { + content = ASCIIUtility.getBytes(is); + } catch (IOException ioex) { + throw new MessagingException("Error reading input stream", ioex); + } + } + + } + + /** + * Constructs a MimeBodyPart using the given header and + * content bytes.

+ * + * Used by providers. + * + * @param headers The header of this part + * @param content bytes representing the body of this part. + * @exception MessagingException for failures + */ + public MimeBodyPart(InternetHeaders headers, byte[] content) + throws MessagingException { + super(); + this.headers = headers; + this.content = content; + } + + /** + * Return the size of the content of this body part in bytes. + * Return -1 if the size cannot be determined.

+ * + * Note that this number may not be an exact measure of the + * content size and may or may not account for any transfer + * encoding of the content.

+ * + * This implementation returns the size of the content + * array (if not null), or, if contentStream is not + * null, and the available method returns a positive + * number, it returns that number as the size. Otherwise, it returns + * -1. + * + * @return size in bytes, or -1 if not known + */ + @Override + public int getSize() throws MessagingException { + if (content != null) + return content.length; + if (contentStream != null) { + try { + int size = contentStream.available(); + // only believe the size if it's greate than zero, since zero + // is the default returned by the InputStream class itself + if (size > 0) + return size; + } catch (IOException ex) { + // ignore it + } + } + return -1; + } + + /** + * Return the number of lines for the content of this Part. + * Return -1 if this number cannot be determined.

+ * + * Note that this number may not be an exact measure of the + * content length and may or may not account for any transfer + * encoding of the content.

+ * + * This implementation returns -1. + * + * @return number of lines, or -1 if not known + */ + @Override + public int getLineCount() throws MessagingException { + return -1; + } + + /** + * Returns the value of the RFC 822 "Content-Type" header field. + * This represents the content type of the content of this + * body part. This value must not be null. If this field is + * unavailable, "text/plain" should be returned.

+ * + * This implementation uses getHeader(name) + * to obtain the requisite header field. + * + * @return Content-Type of this body part + */ + @Override + public String getContentType() throws MessagingException { + String s = getHeader("Content-Type", null); + s = MimeUtil.cleanContentType(this, s); + if (s == null) + s = "text/plain"; + return s; + } + + /** + * Is this Part of the specified MIME type? This method + * compares only the primaryType and + * subType. + * The parameters of the content types are ignored.

+ * + * For example, this method will return true when + * comparing a Part of content type "text/plain" + * with "text/plain; charset=foobar".

+ * + * If the subType of mimeType is the + * special character '*', then the subtype is ignored during the + * comparison. + * + * @exception MessagingException for failures + */ + @Override + public boolean isMimeType(String mimeType) throws MessagingException { + return isMimeType(this, mimeType); + } + + /** + * Returns the disposition from the "Content-Disposition" header field. + * This represents the disposition of this part. The disposition + * describes how the part should be presented to the user.

+ * + * If the Content-Disposition field is unavailable, + * null is returned.

+ * + * This implementation uses getHeader(name) + * to obtain the requisite header field. + * + * @exception MessagingException for failures + * @see #headers + */ + @Override + public String getDisposition() throws MessagingException { + return getDisposition(this); + } + + /** + * Set the disposition in the "Content-Disposition" header field + * of this body part. If the disposition is null, any existing + * "Content-Disposition" header field is removed. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setDisposition(String disposition) throws MessagingException { + setDisposition(this, disposition); + } + + /** + * Returns the content transfer encoding from the + * "Content-Transfer-Encoding" header + * field. Returns null if the header is unavailable + * or its value is absent.

+ * + * This implementation uses getHeader(name) + * to obtain the requisite header field. + * + * @see #headers + */ + @Override + public String getEncoding() throws MessagingException { + return getEncoding(this); + } + + /** + * Returns the value of the "Content-ID" header field. Returns + * null if the field is unavailable or its value is + * absent.

+ * + * This implementation uses getHeader(name) + * to obtain the requisite header field. + */ + @Override + public String getContentID() throws MessagingException { + return getHeader("Content-Id", null); + } + + /** + * Set the "Content-ID" header field of this body part. + * If the cid parameter is null, any existing + * "Content-ID" is removed. + * + * @param cid the Content-ID + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + * @since JavaMail 1.3 + */ + public void setContentID(String cid) throws MessagingException { + if (cid == null) + removeHeader("Content-ID"); + else + setHeader("Content-ID", cid); + } + + /** + * Return the value of the "Content-MD5" header field. Returns + * null if this field is unavailable or its value + * is absent.

+ * + * This implementation uses getHeader(name) + * to obtain the requisite header field. + */ + @Override + public String getContentMD5() throws MessagingException { + return getHeader("Content-MD5", null); + } + + /** + * Set the "Content-MD5" header field of this body part. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + */ + @Override + public void setContentMD5(String md5) throws MessagingException { + setHeader("Content-MD5", md5); + } + + /** + * Get the languages specified in the Content-Language header + * of this MimePart. The Content-Language header is defined by + * RFC 1766. Returns null if this header is not + * available or its value is absent.

+ * + * This implementation uses getHeader(name) + * to obtain the requisite header field. + */ + @Override + public String[] getContentLanguage() throws MessagingException { + return getContentLanguage(this); + } + + /** + * Set the Content-Language header of this MimePart. The + * Content-Language header is defined by RFC 1766. + * + * @param languages array of language tags + */ + @Override + public void setContentLanguage(String[] languages) + throws MessagingException { + setContentLanguage(this, languages); + } + + /** + * Returns the "Content-Description" header field of this body part. + * This typically associates some descriptive information with + * this part. Returns null if this field is unavailable or its + * value is absent.

+ * + * If the Content-Description field is encoded as per RFC 2047, + * it is decoded and converted into Unicode. If the decoding or + * conversion fails, the raw data is returned as is.

+ * + * This implementation uses getHeader(name) + * to obtain the requisite header field. + * + * @return content description + */ + @Override + public String getDescription() throws MessagingException { + return getDescription(this); + } + + /** + * Set the "Content-Description" header field for this body part. + * If the description parameter is null, then any + * existing "Content-Description" fields are removed.

+ * + * If the description contains non US-ASCII characters, it will + * be encoded using the platform's default charset. If the + * description contains only US-ASCII characters, no encoding + * is done and it is used as is.

+ * + * Note that if the charset encoding process fails, a + * MessagingException is thrown, and an UnsupportedEncodingException + * is included in the chain of nested exceptions within the + * MessagingException. + * + * @param description content description + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + * @exception MessagingException otherwise; an + * UnsupportedEncodingException may be included + * in the exception chain if the charset + * conversion fails. + */ + @Override + public void setDescription(String description) throws MessagingException { + setDescription(description, null); + } + + /** + * Set the "Content-Description" header field for this body part. + * If the description parameter is null, then any + * existing "Content-Description" fields are removed.

+ * + * If the description contains non US-ASCII characters, it will + * be encoded using the specified charset. If the description + * contains only US-ASCII characters, no encoding is done and + * it is used as is.

+ * + * Note that if the charset encoding process fails, a + * MessagingException is thrown, and an UnsupportedEncodingException + * is included in the chain of nested exceptions within the + * MessagingException. + * + * @param description Description + * @param charset Charset for encoding + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + * @exception MessagingException otherwise; an + * UnsupportedEncodingException may be included + * in the exception chain if the charset + * conversion fails. + */ + public void setDescription(String description, String charset) + throws MessagingException { + setDescription(this, description, charset); + } + + /** + * Get the filename associated with this body part.

+ * + * Returns the value of the "filename" parameter from the + * "Content-Disposition" header field of this body part. If its + * not available, returns the value of the "name" parameter from + * the "Content-Type" header field of this body part. + * Returns null if both are absent.

+ * + * If the mail.mime.decodefilename System property + * is set to true, the {@link MimeUtility#decodeText + * MimeUtility.decodeText} method will be used to decode the + * filename. While such encoding is not supported by the MIME + * spec, many mailers use this technique to support non-ASCII + * characters in filenames. The default value of this property + * is false. + * + * @return filename + */ + @Override + public String getFileName() throws MessagingException { + return getFileName(this); + } + + /** + * Set the filename associated with this body part, if possible.

+ * + * Sets the "filename" parameter of the "Content-Disposition" + * header field of this body part. For compatibility with older + * mailers, the "name" parameter of the "Content-Type" header is + * also set.

+ * + * If the mail.mime.encodefilename System property + * is set to true, the {@link MimeUtility#encodeText + * MimeUtility.encodeText} method will be used to encode the + * filename. While such encoding is not supported by the MIME + * spec, many mailers use this technique to support non-ASCII + * characters in filenames. The default value of this property + * is false. + * + * @param filename the file name + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setFileName(String filename) throws MessagingException { + setFileName(this, filename); + } + + /** + * Return a decoded input stream for this body part's "content".

+ * + * This implementation obtains the input stream from the DataHandler. + * That is, it invokes getDataHandler().getInputStream(); + * + * @return an InputStream + * @exception IOException this is typically thrown by the + * DataHandler. Refer to the documentation for + * javax.activation.DataHandler for more details. + * @exception MessagingException for other failures + * + * @see #getContentStream + * @see javax.activation.DataHandler#getInputStream + */ + @Override + public InputStream getInputStream() + throws IOException, MessagingException { + return getDataHandler().getInputStream(); + } + + /** + * Produce the raw bytes of the content. This method is used + * when creating a DataHandler object for the content. Subclasses + * that can provide a separate input stream for just the Part + * content might want to override this method.

+ * + * @return an InputStream containing the raw bytes + * @exception MessagingException for failures + * @see #content + * @see MimeMessage#getContentStream + */ + protected InputStream getContentStream() throws MessagingException { + if (contentStream != null) + return ((SharedInputStream)contentStream).newStream(0, -1); + if (content != null) + return new ByteArrayInputStream(content); + + throw new MessagingException("No MimeBodyPart content"); + } + + /** + * Return an InputStream to the raw data with any Content-Transfer-Encoding + * intact. This method is useful if the "Content-Transfer-Encoding" + * header is incorrect or corrupt, which would prevent the + * getInputStream method or getContent method + * from returning the correct data. In such a case the application may + * use this method and attempt to decode the raw data itself.

+ * + * This implementation simply calls the getContentStream + * method. + * + * @return an InputStream containing the raw bytes + * @exception MessagingException for failures + * @see #getInputStream + * @see #getContentStream + * @since JavaMail 1.2 + */ + public InputStream getRawInputStream() throws MessagingException { + return getContentStream(); + } + + /** + * Return a DataHandler for this body part's content.

+ * + * The implementation provided here works just like the + * the implementation in MimeMessage. + * @see MimeMessage#getDataHandler + */ + @Override + public DataHandler getDataHandler() throws MessagingException { + if (dh == null) + dh = new MimePartDataHandler(this); + return dh; + } + + /** + * Return the content as a Java object. The type of the object + * returned is of course dependent on the content itself. For + * example, the native format of a text/plain content is usually + * a String object. The native format for a "multipart" + * content is always a Multipart subclass. For content types that are + * unknown to the DataHandler system, an input stream is returned + * as the content.

+ * + * This implementation obtains the content from the DataHandler. + * That is, it invokes getDataHandler().getContent(); + * If the content is a Multipart or Message object and was created by + * parsing a stream, the object is cached and returned in subsequent + * calls so that modifications to the content will not be lost. + * + * @return Object + * @exception IOException this is typically thrown by the + * DataHandler. Refer to the documentation for + * javax.activation.DataHandler for more details. + * @exception MessagingException for other failures + */ + @Override + public Object getContent() throws IOException, MessagingException { + if (cachedContent != null) + return cachedContent; + Object c; + try { + c = getDataHandler().getContent(); + } catch (FolderClosedIOException fex) { + throw new FolderClosedException(fex.getFolder(), fex.getMessage()); + } catch (MessageRemovedIOException mex) { + throw new MessageRemovedException(mex.getMessage()); + } + if (cacheMultipart && + (c instanceof Multipart || c instanceof Message) && + (content != null || contentStream != null)) { + cachedContent = c; + /* + * We may abandon the input stream so make sure + * the MimeMultipart has consumed the stream. + */ + if (c instanceof MimeMultipart) + ((MimeMultipart)c).parse(); + } + return c; + } + + /** + * This method provides the mechanism to set this body part's content. + * The given DataHandler object should wrap the actual content. + * + * @param dh The DataHandler for the content + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + */ + @Override + public void setDataHandler(DataHandler dh) + throws MessagingException { + this.dh = dh; + cachedContent = null; + MimeBodyPart.invalidateContentHeaders(this); + } + + /** + * A convenience method for setting this body part's content.

+ * + * The content is wrapped in a DataHandler object. Note that a + * DataContentHandler class for the specified type should be + * available to the Jakarta Mail implementation for this to work right. + * That is, to do setContent(foobar, "application/x-foobar"), + * a DataContentHandler for "application/x-foobar" should be installed. + * Refer to the Java Activation Framework for more information. + * + * @param o the content object + * @param type Mime type of the object + * @exception IllegalWriteException if the underlying + * implementation does not support modification of + * existing values + * @exception IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + */ + @Override + public void setContent(Object o, String type) + throws MessagingException { + if (o instanceof Multipart) { + setContent((Multipart)o); + } else { + setDataHandler(new DataHandler(o, type)); + } + } + + /** + * Convenience method that sets the given String as this + * part's content, with a MIME type of "text/plain". If the + * string contains non US-ASCII characters, it will be encoded + * using the platform's default charset. The charset is also + * used to set the "charset" parameter.

+ * + * Note that there may be a performance penalty if + * text is large, since this method may have + * to scan all the characters to determine what charset to + * use.

+ * + * If the charset is already known, use the + * setText method that takes the charset parameter. + * + * @param text the text content to set + * @exception MessagingException if an error occurs + * @see #setText(String text, String charset) + */ + @Override + public void setText(String text) throws MessagingException { + setText(text, null); + } + + /** + * Convenience method that sets the given String as this part's + * content, with a MIME type of "text/plain" and the specified + * charset. The given Unicode string will be charset-encoded + * using the specified charset. The charset is also used to set + * the "charset" parameter. + * + * @param text the text content to set + * @param charset the charset to use for the text + * @exception MessagingException if an error occurs + */ + @Override + public void setText(String text, String charset) + throws MessagingException { + setText(this, text, charset, "plain"); + } + + /** + * Convenience method that sets the given String as this part's + * content, with a primary MIME type of "text" and the specified + * MIME subtype. The given Unicode string will be charset-encoded + * using the specified charset. The charset is also used to set + * the "charset" parameter. + * + * @param text the text content to set + * @param charset the charset to use for the text + * @param subtype the MIME subtype to use (e.g., "html") + * @exception MessagingException if an error occurs + * @since JavaMail 1.4 + */ + @Override + public void setText(String text, String charset, String subtype) + throws MessagingException { + setText(this, text, charset, subtype); + } + + /** + * This method sets the body part's content to a Multipart object. + * + * @param mp The multipart object that is the Message's content + * @exception IllegalWriteException if the underlying + * implementation does not support modification of + * existing values. + * @exception IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + */ + @Override + public void setContent(Multipart mp) throws MessagingException { + setDataHandler(new DataHandler(mp, mp.getContentType())); + mp.setParent(this); + } + + /** + * Use the specified file to provide the data for this part. + * The simple file name is used as the file name for this + * part and the data in the file is used as the data for this + * part. The encoding will be chosen appropriately for the + * file data. The disposition of this part is set to + * {@link Part#ATTACHMENT Part.ATTACHMENT}. + * + * @param file the File object to attach + * @exception IOException errors related to accessing the file + * @exception MessagingException message related errors + * @since JavaMail 1.4 + */ + public void attachFile(File file) throws IOException, MessagingException { + FileDataSource fds = new FileDataSource(file); + this.setDataHandler(new DataHandler(fds)); + this.setFileName(fds.getName()); + this.setDisposition(ATTACHMENT); + } + + /** + * Use the specified file to provide the data for this part. + * The simple file name is used as the file name for this + * part and the data in the file is used as the data for this + * part. The encoding will be chosen appropriately for the + * file data. + * + * @param file the name of the file to attach + * @exception IOException errors related to accessing the file + * @exception MessagingException message related errors + * @since JavaMail 1.4 + */ + public void attachFile(String file) throws IOException, MessagingException { + File f = new File(file); + attachFile(f); + } + + /** + * Use the specified file with the specified Content-Type and + * Content-Transfer-Encoding to provide the data for this part. + * If contentType or encoding are null, appropriate values will + * be chosen. + * The simple file name is used as the file name for this + * part and the data in the file is used as the data for this + * part. The disposition of this part is set to + * {@link Part#ATTACHMENT Part.ATTACHMENT}. + * + * @param file the File object to attach + * @param contentType the Content-Type, or null + * @param encoding the Content-Transfer-Encoding, or null + * @exception IOException errors related to accessing the file + * @exception MessagingException message related errors + * @since JavaMail 1.5 + */ + public void attachFile(File file, String contentType, String encoding) + throws IOException, MessagingException { + DataSource fds = new EncodedFileDataSource(file, contentType, encoding); + this.setDataHandler(new DataHandler(fds)); + this.setFileName(fds.getName()); + this.setDisposition(ATTACHMENT); + } + + /** + * Use the specified file with the specified Content-Type and + * Content-Transfer-Encoding to provide the data for this part. + * If contentType or encoding are null, appropriate values will + * be chosen. + * The simple file name is used as the file name for this + * part and the data in the file is used as the data for this + * part. The disposition of this part is set to + * {@link Part#ATTACHMENT Part.ATTACHMENT}. + * + * @param file the name of the file + * @param contentType the Content-Type, or null + * @param encoding the Content-Transfer-Encoding, or null + * @exception IOException errors related to accessing the file + * @exception MessagingException message related errors + * @since JavaMail 1.5 + */ + public void attachFile(String file, String contentType, String encoding) + throws IOException, MessagingException { + attachFile(new File(file), contentType, encoding); + } + + /** + * A FileDataSource class that allows us to specify the + * Content-Type and Content-Transfer-Encoding. + */ + private static class EncodedFileDataSource extends FileDataSource + implements EncodingAware { + private String contentType; + private String encoding; + + public EncodedFileDataSource(File file, String contentType, + String encoding) { + super(file); + this.contentType = contentType; + this.encoding = encoding; + } + + // overrides DataSource.getContentType() + @Override + public String getContentType() { + return contentType != null ? contentType : super.getContentType(); + } + + // implements EncodingAware.getEncoding() + @Override + public String getEncoding() { + return encoding; + } + } + + /** + * Save the contents of this part in the specified file. The content + * is decoded and saved, without any of the MIME headers. + * + * @param file the File object to write to + * @exception IOException errors related to accessing the file + * @exception MessagingException message related errors + * @since JavaMail 1.4 + */ + public void saveFile(File file) throws IOException, MessagingException { + OutputStream out = null; + InputStream in = null; + try { + out = new BufferedOutputStream(new FileOutputStream(file)); + in = this.getInputStream(); + byte[] buf = new byte[8192]; + int len; + while ((len = in.read(buf)) > 0) + out.write(buf, 0, len); + } finally { + // close streams, but don't mask original exception, if any + try { + if (in != null) + in.close(); + } catch (IOException ex) { } + try { + if (out != null) + out.close(); + } catch (IOException ex) { } + } + } + + /** + * Save the contents of this part in the specified file. The content + * is decoded and saved, without any of the MIME headers. + * + * @param file the name of the file to write to + * @exception IOException errors related to accessing the file + * @exception MessagingException message related errors + * @since JavaMail 1.4 + */ + public void saveFile(String file) throws IOException, MessagingException { + File f = new File(file); + saveFile(f); + } + + /** + * Output the body part as an RFC 822 format stream. + * + * @exception IOException if an error occurs writing to the + * stream or if an error is generated + * by the javax.activation layer. + * @exception MessagingException for other failures + * @see javax.activation.DataHandler#writeTo + */ + @Override + public void writeTo(OutputStream os) + throws IOException, MessagingException { + writeTo(this, os, null); + } + + /** + * Get all the headers for this header_name. Note that certain + * headers may be encoded as per RFC 2047 if they contain + * non US-ASCII characters and these should be decoded. + * + * @param name name of header + * @return array of headers + * @see javax.mail.internet.MimeUtility + */ + @Override + public String[] getHeader(String name) throws MessagingException { + return headers.getHeader(name); + } + + /** + * Get all the headers for this header name, returned as a single + * String, with headers separated by the delimiter. If the + * delimiter is null, only the first header is + * returned. + * + * @param name the name of this header + * @param delimiter delimiter between fields in returned string + * @return the value fields for all headers with + * this name + * @exception MessagingException for failures + */ + @Override + public String getHeader(String name, String delimiter) + throws MessagingException { + return headers.getHeader(name, delimiter); + } + + /** + * Set the value for this header_name. Replaces all existing + * header values with this new value. Note that RFC 822 headers + * must contain only US-ASCII characters, so a header that + * contains non US-ASCII characters must be encoded as per the + * rules of RFC 2047. + * + * @param name header name + * @param value header value + * @see javax.mail.internet.MimeUtility + */ + @Override + public void setHeader(String name, String value) + throws MessagingException { + headers.setHeader(name, value); + } + + /** + * Add this value to the existing values for this header_name. + * Note that RFC 822 headers must contain only US-ASCII + * characters, so a header that contains non US-ASCII characters + * must be encoded as per the rules of RFC 2047. + * + * @param name header name + * @param value header value + * @see javax.mail.internet.MimeUtility + */ + @Override + public void addHeader(String name, String value) + throws MessagingException { + headers.addHeader(name, value); + } + + /** + * Remove all headers with this name. + */ + @Override + public void removeHeader(String name) throws MessagingException { + headers.removeHeader(name); + } + + /** + * Return all the headers from this Message as an Enumeration of + * Header objects. + */ + @Override + public Enumeration

getAllHeaders() throws MessagingException { + return headers.getAllHeaders(); + } + + /** + * Return matching headers from this Message as an Enumeration of + * Header objects. + */ + @Override + public Enumeration
getMatchingHeaders(String[] names) + throws MessagingException { + return headers.getMatchingHeaders(names); + } + + /** + * Return non-matching headers from this Message as an + * Enumeration of Header objects. + */ + @Override + public Enumeration
getNonMatchingHeaders(String[] names) + throws MessagingException { + return headers.getNonMatchingHeaders(names); + } + + /** + * Add a header line to this body part + */ + @Override + public void addHeaderLine(String line) throws MessagingException { + headers.addHeaderLine(line); + } + + /** + * Get all header lines as an Enumeration of Strings. A Header + * line is a raw RFC 822 header line, containing both the "name" + * and "value" field. + */ + @Override + public Enumeration getAllHeaderLines() throws MessagingException { + return headers.getAllHeaderLines(); + } + + /** + * Get matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC 822 header line, containing both + * the "name" and "value" field. + */ + @Override + public Enumeration getMatchingHeaderLines(String[] names) + throws MessagingException { + return headers.getMatchingHeaderLines(names); + } + + /** + * Get non-matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC 822 header line, containing both + * the "name" and "value" field. + */ + @Override + public Enumeration getNonMatchingHeaderLines(String[] names) + throws MessagingException { + return headers.getNonMatchingHeaderLines(names); + } + + /** + * Examine the content of this body part and update the appropriate + * MIME headers. Typical headers that get set here are + * Content-Type and Content-Transfer-Encoding. + * Headers might need to be updated in two cases: + * + *
+ * - A message being crafted by a mail application will certainly + * need to activate this method at some point to fill up its internal + * headers. + * + *
+ * - A message read in from a Store will have obtained + * all its headers from the store, and so doesn't need this. + * However, if this message is editable and if any edits have + * been made to either the content or message structure, we might + * need to resync our headers. + * + *
+ * In both cases this method is typically called by the + * Message.saveChanges method.

+ * + * If the {@link #cachedContent} field is not null (that is, + * it references a Multipart or Message object), then + * that object is used to set a new DataHandler, any + * stream data used to create this object is discarded, + * and the {@link #cachedContent} field is cleared. + * + * @exception MessagingException for failures + */ + protected void updateHeaders() throws MessagingException { + updateHeaders(this); + /* + * If we've cached a Multipart or Message object then + * we're now committed to using this instance of the + * object and we discard any stream data used to create + * this object. + */ + if (cachedContent != null) { + dh = new DataHandler(cachedContent, getContentType()); + cachedContent = null; + content = null; + if (contentStream != null) { + try { + contentStream.close(); + } catch (IOException ioex) { } // nothing to do + } + contentStream = null; + } + } + + ///////////////////////////////////////////////////////////// + // Package private convenience methods to share code among // + // MimeMessage and MimeBodyPart // + ///////////////////////////////////////////////////////////// + + static boolean isMimeType(MimePart part, String mimeType) + throws MessagingException { + // XXX - lots of room for optimization here! + String type = part.getContentType(); + try { + return new ContentType(type).match(mimeType); + } catch (ParseException ex) { + // we only need the type and subtype so throw away the rest + try { + int i = type.indexOf(';'); + if (i > 0) + return new ContentType(type.substring(0, i)).match(mimeType); + } catch (ParseException pex2) { + } + return type.equalsIgnoreCase(mimeType); + } + } + + static void setText(MimePart part, String text, String charset, + String subtype) throws MessagingException { + if (charset == null) { + if (MimeUtility.checkAscii(text) != MimeUtility.ALL_ASCII) + charset = MimeUtility.getDefaultMIMECharset(); + else + charset = "us-ascii"; + } + // XXX - should at least ensure that subtype is an atom + part.setContent(text, "text/" + subtype + "; charset=" + + MimeUtility.quote(charset, HeaderTokenizer.MIME)); + } + + static String getDisposition(MimePart part) throws MessagingException { + String s = part.getHeader("Content-Disposition", null); + + if (s == null) + return null; + + ContentDisposition cd = new ContentDisposition(s); + return cd.getDisposition(); + } + + static void setDisposition(MimePart part, String disposition) + throws MessagingException { + if (disposition == null) + part.removeHeader("Content-Disposition"); + else { + String s = part.getHeader("Content-Disposition", null); + if (s != null) { + /* A Content-Disposition header already exists .. + * + * Override disposition, but attempt to retain + * existing disposition parameters + */ + ContentDisposition cd = new ContentDisposition(s); + cd.setDisposition(disposition); + disposition = cd.toString(); + } + part.setHeader("Content-Disposition", disposition); + } + } + + static String getDescription(MimePart part) + throws MessagingException { + String rawvalue = part.getHeader("Content-Description", null); + + if (rawvalue == null) + return null; + + try { + return MimeUtility.decodeText(MimeUtility.unfold(rawvalue)); + } catch (UnsupportedEncodingException ex) { + return rawvalue; + } + } + + static void + setDescription(MimePart part, String description, String charset) + throws MessagingException { + if (description == null) { + part.removeHeader("Content-Description"); + return; + } + + try { + part.setHeader("Content-Description", MimeUtility.fold(21, + MimeUtility.encodeText(description, charset, null))); + } catch (UnsupportedEncodingException uex) { + throw new MessagingException("Encoding error", uex); + } + } + + static String getFileName(MimePart part) throws MessagingException { + String filename = null; + String s = part.getHeader("Content-Disposition", null); + + if (s != null) { + // Parse the header .. + ContentDisposition cd = new ContentDisposition(s); + filename = cd.getParameter("filename"); + } + if (filename == null) { + // Still no filename ? Try the "name" ContentType parameter + s = part.getHeader("Content-Type", null); + s = MimeUtil.cleanContentType(part, s); + if (s != null) { + try { + ContentType ct = new ContentType(s); + filename = ct.getParameter("name"); + } catch (ParseException pex) { } // ignore it + } + } + if (decodeFileName && filename != null) { + try { + filename = MimeUtility.decodeText(filename); + } catch (UnsupportedEncodingException ex) { + throw new MessagingException("Can't decode filename", ex); + } + } + return filename; + } + + static void setFileName(MimePart part, String name) + throws MessagingException { + if (encodeFileName && name != null) { + try { + name = MimeUtility.encodeText(name); + } catch (UnsupportedEncodingException ex) { + throw new MessagingException("Can't encode filename", ex); + } + } + + // Set the Content-Disposition "filename" parameter + String s = part.getHeader("Content-Disposition", null); + ContentDisposition cd = + new ContentDisposition(s == null ? Part.ATTACHMENT : s); + // ensure that the filename is encoded if necessary + String charset = MimeUtility.getDefaultMIMECharset(); + ParameterList p = cd.getParameterList(); + if (p == null) { + p = new ParameterList(); + cd.setParameterList(p); + } + if (encodeFileName) + p.setLiteral("filename", name); + else + p.set("filename", name, charset); + part.setHeader("Content-Disposition", cd.toString()); + + /* + * Also attempt to set the Content-Type "name" parameter, + * to satisfy ancient MUAs. XXX - This is not RFC compliant. + */ + if (setContentTypeFileName) { + s = part.getHeader("Content-Type", null); + s = MimeUtil.cleanContentType(part, s); + if (s != null) { + try { + ContentType cType = new ContentType(s); + // ensure that the filename is encoded if necessary + p = cType.getParameterList(); + if (p == null) { + p = new ParameterList(); + cType.setParameterList(p); + } + if (encodeFileName) + p.setLiteral("name", name); + else + p.set("name", name, charset); + part.setHeader("Content-Type", cType.toString()); + } catch (ParseException pex) { } // ignore it + } + } + } + + static String[] getContentLanguage(MimePart part) + throws MessagingException { + String s = part.getHeader("Content-Language", null); + + if (s == null) + return null; + + // Tokenize the header to obtain the Language-tags (skip comments) + HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME); + List v = new ArrayList<>(); + + HeaderTokenizer.Token tk; + int tkType; + + while (true) { + tk = h.next(); // get a language-tag + tkType = tk.getType(); + if (tkType == HeaderTokenizer.Token.EOF) + break; // done + else if (tkType == HeaderTokenizer.Token.ATOM) + v.add(tk.getValue()); + else // invalid token, skip it. + continue; + } + + if (v.isEmpty()) + return null; + + String[] language = new String[v.size()]; + v.toArray(language); + return language; + } + + static void setContentLanguage(MimePart part, String[] languages) + throws MessagingException { + StringBuilder sb = new StringBuilder(languages[0]); + int len = "Content-Language".length() + 2 + languages[0].length(); + for (int i = 1; i < languages.length; i++) { + sb.append(','); + len++; + if (len > 76) { + sb.append("\r\n\t"); + len = 8; + } + sb.append(languages[i]); + len += languages[i].length(); + } + part.setHeader("Content-Language", sb.toString()); + } + + static String getEncoding(MimePart part) throws MessagingException { + String s = part.getHeader("Content-Transfer-Encoding", null); + + if (s == null) + return null; + + s = s.trim(); // get rid of trailing spaces + if (s.length() == 0) + return null; + // quick check for known values to avoid unnecessary use + // of tokenizer. + if (s.equalsIgnoreCase("7bit") || s.equalsIgnoreCase("8bit") || + s.equalsIgnoreCase("quoted-printable") || + s.equalsIgnoreCase("binary") || + s.equalsIgnoreCase("base64")) + return s; + + // Tokenize the header to obtain the encoding (skip comments) + HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME); + + HeaderTokenizer.Token tk; + int tkType; + + for (;;) { + tk = h.next(); // get a token + tkType = tk.getType(); + if (tkType == HeaderTokenizer.Token.EOF) + break; // done + else if (tkType == HeaderTokenizer.Token.ATOM) + return tk.getValue(); + else // invalid token, skip it. + continue; + } + return s; + } + + static void setEncoding(MimePart part, String encoding) + throws MessagingException { + part.setHeader("Content-Transfer-Encoding", encoding); + } + + /** + * Restrict the encoding to values allowed for the + * Content-Type of the specified MimePart. Returns + * either the original encoding or null. + */ + static String restrictEncoding(MimePart part, String encoding) + throws MessagingException { + if (!ignoreMultipartEncoding || encoding == null) + return encoding; + + if (encoding.equalsIgnoreCase("7bit") || + encoding.equalsIgnoreCase("8bit") || + encoding.equalsIgnoreCase("binary")) + return encoding; // these encodings are always valid + + String type = part.getContentType(); + if (type == null) + return encoding; + + try { + /* + * multipart and message types aren't allowed to have + * encodings except for the three mentioned above. + * If it's one of these types, ignore the encoding. + */ + ContentType cType = new ContentType(type); + if (cType.match("multipart/*")) + return null; + if (cType.match("message/*") && + !PropUtil.getBooleanSystemProperty( + "mail.mime.allowencodedmessages", false)) + return null; + } catch (ParseException pex) { + // ignore it + } + return encoding; + } + + static void updateHeaders(MimePart part) throws MessagingException { + DataHandler dh = part.getDataHandler(); + if (dh == null) // Huh ? + return; + + try { + String type = dh.getContentType(); + boolean composite = false; + boolean needCTHeader = part.getHeader("Content-Type") == null; + + ContentType cType = new ContentType(type); + + /* + * If this is a multipart, give sub-parts a chance to + * update their headers. Even though the data for this + * multipart may have come from a stream, one of the + * sub-parts may have been updated. + */ + if (cType.match("multipart/*")) { + // If multipart, recurse + composite = true; + Object o; + if (part instanceof MimeBodyPart) { + MimeBodyPart mbp = (MimeBodyPart)part; + o = mbp.cachedContent != null ? + mbp.cachedContent : dh.getContent(); + } else if (part instanceof MimeMessage) { + MimeMessage msg = (MimeMessage)part; + o = msg.cachedContent != null ? + msg.cachedContent : dh.getContent(); + } else + o = dh.getContent(); + if (o instanceof MimeMultipart) + ((MimeMultipart)o).updateHeaders(); + else + throw new MessagingException("MIME part of type \"" + + type + "\" contains object of type " + + o.getClass().getName() + " instead of MimeMultipart"); + } else if (cType.match("message/rfc822")) { + composite = true; + // XXX - call MimeMessage.updateHeaders()? + } + + /* + * If this is our own MimePartDataHandler, we can't update any + * of the headers. + * + * If this is a MimePartDataHandler coming from another part, + * we need to copy over the content headers from the other part. + * Note that the MimePartDataHandler still refers to the original + * data and the original MimePart. + */ + if (dh instanceof MimePartDataHandler) { + MimePartDataHandler mdh = (MimePartDataHandler)dh; + MimePart mpart = mdh.getPart(); + if (mpart != part) { + if (needCTHeader) + part.setHeader("Content-Type", mpart.getContentType()); + // XXX - can't change the encoding of the data from the + // other part without decoding and reencoding it, so + // we just force it to match the original, but if the + // original has no encoding we'll consider reencoding it + String enc = mpart.getEncoding(); + if (enc != null) { + setEncoding(part, enc); + return; + } + } else + return; + } + + // Content-Transfer-Encoding, but only if we don't + // already have one + if (!composite) { // not allowed on composite parts + if (part.getHeader("Content-Transfer-Encoding") == null) + setEncoding(part, MimeUtility.getEncoding(dh)); + + if (needCTHeader && setDefaultTextCharset && + cType.match("text/*") && + cType.getParameter("charset") == null) { + /* + * Set a default charset for text parts. + * We really should examine the data to determine + * whether or not it's all ASCII, but that's too + * expensive so we make an assumption: If we + * chose 7bit encoding for this data, it's probably + * ASCII. (MimeUtility.getEncoding will choose + * 7bit only in this case, but someone might've + * set the Content-Transfer-Encoding header manually.) + */ + String charset; + String enc = part.getEncoding(); + if (enc != null && enc.equalsIgnoreCase("7bit")) + charset = "us-ascii"; + else + charset = MimeUtility.getDefaultMIMECharset(); + cType.setParameter("charset", charset); + type = cType.toString(); + } + } + + // Now, let's update our own headers ... + + // Content-type, but only if we don't already have one + if (needCTHeader) { + /* + * Pull out "filename" from Content-Disposition, and + * use that to set the "name" parameter. This is to + * satisfy older MUAs (DtMail, Roam and probably + * a bunch of others). + */ + if (setContentTypeFileName) { + String s = part.getHeader("Content-Disposition", null); + if (s != null) { + // Parse the header .. + ContentDisposition cd = new ContentDisposition(s); + String filename = cd.getParameter("filename"); + if (filename != null) { + ParameterList p = cType.getParameterList(); + if (p == null) { + p = new ParameterList(); + cType.setParameterList(p); + } + if (encodeFileName) + p.setLiteral("name", + MimeUtility.encodeText(filename)); + else + p.set("name", filename, + MimeUtility.getDefaultMIMECharset()); + type = cType.toString(); + } + } + } + + part.setHeader("Content-Type", type); + } + } catch (IOException ex) { + throw new MessagingException("IOException updating headers", ex); + } + } + + static void invalidateContentHeaders(MimePart part) + throws MessagingException { + part.removeHeader("Content-Type"); + part.removeHeader("Content-Transfer-Encoding"); + } + + static void writeTo(MimePart part, OutputStream os, String[] ignoreList) + throws IOException, MessagingException { + + // see if we already have a LOS + LineOutputStream los = null; + if (os instanceof LineOutputStream) { + los = (LineOutputStream) os; + } else { + los = new LineOutputStream(os, allowutf8); + } + + // First, write out the header + Enumeration hdrLines + = part.getNonMatchingHeaderLines(ignoreList); + while (hdrLines.hasMoreElements()) + los.writeln(hdrLines.nextElement()); + + // The CRLF separator between header and content + los.writeln(); + + // Finally, the content. Encode if required. + // XXX: May need to account for ESMTP ? + InputStream is = null; + byte[] buf = null; + try { + /* + * If the data for this part comes from a stream, + * and is already encoded, + * just copy it to the output stream without decoding + * and reencoding it. + */ + DataHandler dh = part.getDataHandler(); + if (dh instanceof MimePartDataHandler) { + MimePartDataHandler mpdh = (MimePartDataHandler)dh; + MimePart mpart = mpdh.getPart(); + if (mpart.getEncoding() != null) + is = mpdh.getContentStream(); + } + if (is != null) { + // now copy the data to the output stream + buf = new byte[8192]; + int len; + while ((len = is.read(buf)) > 0) + os.write(buf, 0, len); + } else { + os = MimeUtility.encode(os, + restrictEncoding(part, part.getEncoding())); + part.getDataHandler().writeTo(os); + } + } finally { + if (is != null) + is.close(); + buf = null; + } + os.flush(); // Needed to complete encoding + } + + /** + * A special DataHandler used only as a marker to indicate that + * the source of the data is a MimePart (that is, a byte array + * or a stream). This prevents updateHeaders from trying to + * change the headers for such data. In particular, the original + * Content-Transfer-Encoding for the data must be preserved. + * Otherwise the data would need to be decoded and reencoded. + */ + static class MimePartDataHandler extends DataHandler { + MimePart part; + public MimePartDataHandler(MimePart part) { + super(new MimePartDataSource(part)); + this.part = part; + } + + InputStream getContentStream() throws MessagingException { + InputStream is = null; + + if (part instanceof MimeBodyPart) { + MimeBodyPart mbp = (MimeBodyPart)part; + is = mbp.getContentStream(); + } else if (part instanceof MimeMessage) { + MimeMessage msg = (MimeMessage)part; + is = msg.getContentStream(); + } + return is; + } + + MimePart getPart() { + return part; + } + } +} diff --git a/app/src/main/java/javax/mail/internet/MimeMessage.java b/app/src/main/java/javax/mail/internet/MimeMessage.java new file mode 100644 index 0000000000..74bb4b296c --- /dev/null +++ b/app/src/main/java/javax/mail/internet/MimeMessage.java @@ -0,0 +1,2290 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import javax.mail.*; +import javax.activation.*; +import java.lang.*; +import java.io.*; +import java.util.*; +import java.text.ParseException; +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.util.MimeUtil; +import com.sun.mail.util.MessageRemovedIOException; +import com.sun.mail.util.FolderClosedIOException; +import com.sun.mail.util.LineOutputStream; +import javax.mail.util.SharedByteArrayInputStream; + +/** + * This class represents a MIME style email message. It implements + * the Message abstract class and the MimePart + * interface.

+ * + * Clients wanting to create new MIME style messages will instantiate + * an empty MimeMessage object and then fill it with appropriate + * attributes and content.

+ * + * Service providers that implement MIME compliant backend stores may + * want to subclass MimeMessage and override certain methods to provide + * specific implementations. The simplest case is probably a provider + * that generates a MIME style input stream and leaves the parsing of + * the stream to this class.

+ * + * MimeMessage uses the InternetHeaders class to parse and + * store the top level RFC 822 headers of a message.

+ * + * The mail.mime.address.strict session property controls + * the parsing of address headers. By default, strict parsing of address + * headers is done. If this property is set to "false", + * strict parsing is not done and many illegal addresses that sometimes + * occur in real messages are allowed. See the InternetAddress + * class for details. + * + *


A note on RFC 822 and MIME headers

+ * + * RFC 822 header fields must contain only + * US-ASCII characters. MIME allows non ASCII characters to be present + * in certain portions of certain headers, by encoding those characters. + * RFC 2047 specifies the rules for doing this. The MimeUtility + * class provided in this package can be used to to achieve this. + * Callers of the setHeader, addHeader, and + * addHeaderLine methods are responsible for enforcing + * the MIME requirements for the specified headers. In addition, these + * header fields must be folded (wrapped) before being sent if they + * exceed the line length limitation for the transport (1000 bytes for + * SMTP). Received headers may have been folded. The application is + * responsible for folding and unfolding headers as appropriate.

+ * + * @author John Mani + * @author Bill Shannon + * @author Max Spivak + * @author Kanwar Oberoi + * @see javax.mail.internet.MimeUtility + * @see javax.mail.Part + * @see javax.mail.Message + * @see javax.mail.internet.MimePart + * @see javax.mail.internet.InternetAddress + */ + +public class MimeMessage extends Message implements MimePart { + + /** + * The DataHandler object representing this Message's content. + */ + protected DataHandler dh; + + /** + * Byte array that holds the bytes of this Message's content. + */ + protected byte[] content; + + /** + * If the data for this message was supplied by an + * InputStream that implements the SharedInputStream interface, + * contentStream is another such stream representing + * the content of this message. In this case, content + * will be null. + * + * @since JavaMail 1.2 + */ + protected InputStream contentStream; + + /** + * The InternetHeaders object that stores the header + * of this message. + */ + protected InternetHeaders headers; + + /** + * The Flags for this message. + */ + protected Flags flags; + + /** + * A flag indicating whether the message has been modified. + * If the message has not been modified, any data in the + * content array is assumed to be valid and is used + * directly in the writeTo method. This flag is + * set to true when an empty message is created or when the + * saveChanges method is called. + * + * @since JavaMail 1.2 + */ + protected boolean modified = false; + + /** + * Does the saveChanges method need to be called on + * this message? This flag is set to false by the public constructor + * and set to true by the saveChanges method. The + * writeTo method checks this flag and calls the + * saveChanges method as necessary. This avoids the + * common mistake of forgetting to call the saveChanges + * method on a newly constructed message. + * + * @since JavaMail 1.2 + */ + protected boolean saved = false; + + /** + * If our content is a Multipart or Message object, we save it + * the first time it's created by parsing a stream so that changes + * to the contained objects will not be lost.

+ * + * If this field is not null, it's return by the {@link #getContent} + * method. The {@link #getContent} method sets this field if it + * would return a Multipart or MimeMessage object. This field is + * is cleared by the {@link #setDataHandler} method. + * + * @since JavaMail 1.5 + */ + protected Object cachedContent; + + // Used to parse dates + private static final MailDateFormat mailDateFormat = new MailDateFormat(); + + // Should addresses in headers be parsed in "strict" mode? + private boolean strict = true; + // Is UTF-8 allowed in headers? + private boolean allowutf8 = false; + + /** + * Default constructor. An empty message object is created. + * The headers field is set to an empty InternetHeaders + * object. The flags field is set to an empty Flags + * object. The modified flag is set to true. + * + * @param session the Sesssion + */ + public MimeMessage(Session session) { + super(session); + modified = true; + headers = new InternetHeaders(); + flags = new Flags(); // empty flags object + initStrict(); + } + + /** + * Constructs a MimeMessage by reading and parsing the data from the + * specified MIME InputStream. The InputStream will be left positioned + * at the end of the data for the message. Note that the input stream + * parse is done within this constructor itself.

+ * + * The input stream contains an entire MIME formatted message with + * headers and data. + * + * @param session Session object for this message + * @param is the message input stream + * @exception MessagingException for failures + */ + public MimeMessage(Session session, InputStream is) + throws MessagingException { + super(session); + flags = new Flags(); // empty Flags object + initStrict(); + parse(is); + saved = true; + } + + /** + * Constructs a new MimeMessage with content initialized from the + * source MimeMessage. The new message is independent + * of the original.

+ * + * Note: The current implementation is rather inefficient, copying + * the data more times than strictly necessary. + * + * @param source the message to copy content from + * @exception MessagingException for failures + * @since JavaMail 1.2 + */ + public MimeMessage(MimeMessage source) throws MessagingException { + super(source.session); + flags = source.getFlags(); + if (flags == null) // make sure flags is always set + flags = new Flags(); + ByteArrayOutputStream bos; + int size = source.getSize(); + if (size > 0) + bos = new ByteArrayOutputStream(size); + else + bos = new ByteArrayOutputStream(); + try { + strict = source.strict; + source.writeTo(bos); + bos.close(); + SharedByteArrayInputStream bis = + new SharedByteArrayInputStream(bos.toByteArray()); + parse(bis); + bis.close(); + saved = true; + } catch (IOException ex) { + // should never happen, but just in case... + throw new MessagingException("IOException while copying message", + ex); + } + } + + /** + * Constructs an empty MimeMessage object with the given Folder + * and message number.

+ * + * This method is for providers subclassing MimeMessage. + * + * @param folder the Folder this message is from + * @param msgnum the number of this message + */ + protected MimeMessage(Folder folder, int msgnum) { + super(folder, msgnum); + flags = new Flags(); // empty Flags object + saved = true; + initStrict(); + } + + /** + * Constructs a MimeMessage by reading and parsing the data from the + * specified MIME InputStream. The InputStream will be left positioned + * at the end of the data for the message. Note that the input stream + * parse is done within this constructor itself.

+ * + * This method is for providers subclassing MimeMessage. + * + * @param folder The containing folder. + * @param is the message input stream + * @param msgnum Message number of this message within its folder + * @exception MessagingException for failures + */ + protected MimeMessage(Folder folder, InputStream is, int msgnum) + throws MessagingException { + this(folder, msgnum); + initStrict(); + parse(is); + } + + /** + * Constructs a MimeMessage from the given InternetHeaders object + * and content. + * + * This method is for providers subclassing MimeMessage. + * + * @param folder The containing folder. + * @param headers The headers + * @param content The message content + * @param msgnum Message number of this message within its folder + * @exception MessagingException for failures + */ + protected MimeMessage(Folder folder, InternetHeaders headers, + byte[] content, int msgnum) throws MessagingException { + this(folder, msgnum); + this.headers = headers; + this.content = content; + initStrict(); + } + + /** + * Set the strict flag based on property. + */ + private void initStrict() { + if (session != null) { + Properties props = session.getProperties(); + strict = PropUtil.getBooleanProperty(props, + "mail.mime.address.strict", true); + allowutf8 = PropUtil.getBooleanProperty(props, + "mail.mime.allowutf8", false); + } + } + + /** + * Parse the InputStream setting the headers and + * content fields appropriately. Also resets the + * modified flag.

+ * + * This method is intended for use by subclasses that need to + * control when the InputStream is parsed. + * + * @param is The message input stream + * @exception MessagingException for failures + */ + protected void parse(InputStream is) throws MessagingException { + + if (!(is instanceof ByteArrayInputStream) && + !(is instanceof BufferedInputStream) && + !(is instanceof SharedInputStream)) + is = new BufferedInputStream(is); + + headers = createInternetHeaders(is); + + if (is instanceof SharedInputStream) { + SharedInputStream sis = (SharedInputStream)is; + contentStream = sis.newStream(sis.getPosition(), -1); + } else { + try { + content = ASCIIUtility.getBytes(is); + } catch (IOException ioex) { + throw new MessagingException("IOException", ioex); + } + } + + modified = false; + } + + /** + * Returns the value of the RFC 822 "From" header fields. If this + * header field is absent, the "Sender" header field is used. + * If the "Sender" header field is also absent, null + * is returned.

+ * + * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return Address object + * @exception MessagingException for failures + * @see #headers + */ + @Override + public Address[] getFrom() throws MessagingException { + Address[] a = getAddressHeader("From"); + if (a == null) + a = getAddressHeader("Sender"); + + return a; + } + + /** + * Set the RFC 822 "From" header field. Any existing values are + * replaced with the given address. If address is null, + * this header is removed. + * + * @param address the sender of this message + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setFrom(Address address) throws MessagingException { + setAddressHeader("From", new Address[] { address }); + } + + /** + * Set the RFC 822 "From" header field. Any existing values are + * replaced with the given addresses. If address is null, + * this header is removed. + * + * @param address the sender(s) of this message + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + * @since JvaMail 1.5 + */ + public void setFrom(String address) throws MessagingException { + setAddressHeader("From", InternetAddress.parse(address)); + } + + /** + * Set the RFC 822 "From" header field using the value of the + * InternetAddress.getLocalAddress method. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setFrom() throws MessagingException { + InternetAddress me = null; + try { + me = InternetAddress._getLocalAddress(session); + } catch (Exception ex) { + // if anything goes wrong (SecurityException, UnknownHostException), + // chain the exception + throw new MessagingException("No From address", ex); + } + if (me != null) + setFrom(me); + else + throw new MessagingException("No From address"); + } + + /** + * Add the specified addresses to the existing "From" field. If + * the "From" field does not already exist, it is created. + * + * @param addresses the senders of this message + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void addFrom(Address[] addresses) throws MessagingException { + addAddressHeader("From", addresses); + } + + /** + * Returns the value of the RFC 822 "Sender" header field. + * If the "Sender" header field is absent, null + * is returned.

+ * + * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return Address object + * @exception MessagingException for failures + * @see #headers + * @since JavaMail 1.3 + */ + public Address getSender() throws MessagingException { + Address[] a = getAddressHeader("Sender"); + if (a == null || a.length == 0) + return null; + return a[0]; // there can be only one + } + + /** + * Set the RFC 822 "Sender" header field. Any existing values are + * replaced with the given address. If address is null, + * this header is removed. + * + * @param address the sender of this message + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + * @since JavaMail 1.3 + */ + public void setSender(Address address) throws MessagingException { + setAddressHeader("Sender", new Address[] { address }); + } + + /** + * This inner class extends the javax.mail.Message.RecipientType + * class to add additional RecipientTypes. The one additional + * RecipientType currently defined here is NEWSGROUPS. + * + * @see javax.mail.Message.RecipientType + */ + public static class RecipientType extends Message.RecipientType { + + private static final long serialVersionUID = -5468290701714395543L; + + /** + * The "Newsgroup" (Usenet news) recipients. + */ + public static final RecipientType NEWSGROUPS = + new RecipientType("Newsgroups"); + protected RecipientType(String type) { + super(type); + } + + @Override + protected Object readResolve() throws ObjectStreamException { + if (type.equals("Newsgroups")) + return NEWSGROUPS; + else + return super.readResolve(); + } + } + + /** + * Returns the recepients specified by the type. The mapping + * between the type and the corresponding RFC 822 header is + * as follows: + *

+     *		Message.RecipientType.TO		"To"
+     *		Message.RecipientType.CC		"Cc"
+     *		Message.RecipientType.BCC		"Bcc"
+     *		MimeMessage.RecipientType.NEWSGROUPS	"Newsgroups"
+     * 

+ * + * Returns null if the header specified by the type is not found + * or if its value is empty.

+ * + * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @param type Type of recepient + * @return array of Address objects + * @exception MessagingException if header could not + * be retrieved + * @exception AddressException if the header is misformatted + * @see #headers + * @see javax.mail.Message.RecipientType#TO + * @see javax.mail.Message.RecipientType#CC + * @see javax.mail.Message.RecipientType#BCC + * @see javax.mail.internet.MimeMessage.RecipientType#NEWSGROUPS + */ + @Override + public Address[] getRecipients(Message.RecipientType type) + throws MessagingException { + if (type == RecipientType.NEWSGROUPS) { + String s = getHeader("Newsgroups", ","); + return (s == null) ? null : NewsAddress.parse(s); + } else + return getAddressHeader(getHeaderName(type)); + } + + /** + * Get all the recipient addresses for the message. + * Extracts the TO, CC, BCC, and NEWSGROUPS recipients. + * + * @return array of Address objects + * @exception MessagingException for failures + * @see javax.mail.Message.RecipientType#TO + * @see javax.mail.Message.RecipientType#CC + * @see javax.mail.Message.RecipientType#BCC + * @see javax.mail.internet.MimeMessage.RecipientType#NEWSGROUPS + */ + @Override + public Address[] getAllRecipients() throws MessagingException { + Address[] all = super.getAllRecipients(); + Address[] ng = getRecipients(RecipientType.NEWSGROUPS); + + if (ng == null) + return all; // the common case + if (all == null) + return ng; // a rare case + + Address[] addresses = new Address[all.length + ng.length]; + System.arraycopy(all, 0, addresses, 0, all.length); + System.arraycopy(ng, 0, addresses, all.length, ng.length); + return addresses; + } + + /** + * Set the specified recipient type to the given addresses. + * If the address parameter is null, the corresponding + * recipient field is removed. + * + * @param type Recipient type + * @param addresses Addresses + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + * @see #getRecipients + */ + @Override + public void setRecipients(Message.RecipientType type, Address[] addresses) + throws MessagingException { + if (type == RecipientType.NEWSGROUPS) { + if (addresses == null || addresses.length == 0) + removeHeader("Newsgroups"); + else + setHeader("Newsgroups", NewsAddress.toString(addresses)); + } else + setAddressHeader(getHeaderName(type), addresses); + } + + /** + * Set the specified recipient type to the given addresses. + * If the address parameter is null, the corresponding + * recipient field is removed. + * + * @param type Recipient type + * @param addresses Addresses + * @exception AddressException if the attempt to parse the + * addresses String fails + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + * @see #getRecipients + * @since JavaMail 1.2 + */ + public void setRecipients(Message.RecipientType type, String addresses) + throws MessagingException { + if (type == RecipientType.NEWSGROUPS) { + if (addresses == null || addresses.length() == 0) + removeHeader("Newsgroups"); + else + setHeader("Newsgroups", addresses); + } else + setAddressHeader(getHeaderName(type), + addresses == null ? null : InternetAddress.parse(addresses)); + } + + /** + * Add the given addresses to the specified recipient type. + * + * @param type Recipient type + * @param addresses Addresses + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void addRecipients(Message.RecipientType type, Address[] addresses) + throws MessagingException { + if (type == RecipientType.NEWSGROUPS) { + String s = NewsAddress.toString(addresses); + if (s != null) + addHeader("Newsgroups", s); + } else + addAddressHeader(getHeaderName(type), addresses); + } + + /** + * Add the given addresses to the specified recipient type. + * + * @param type Recipient type + * @param addresses Addresses + * @exception AddressException if the attempt to parse the + * addresses String fails + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + * @since JavaMail 1.2 + */ + public void addRecipients(Message.RecipientType type, String addresses) + throws MessagingException { + if (type == RecipientType.NEWSGROUPS) { + if (addresses != null && addresses.length() != 0) + addHeader("Newsgroups", addresses); + } else + addAddressHeader(getHeaderName(type), + InternetAddress.parse(addresses)); + } + + /** + * Return the value of the RFC 822 "Reply-To" header field. If + * this header is unavailable or its value is absent, then + * the getFrom method is called and its value is returned. + * + * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @exception MessagingException for failures + * @see #headers + */ + @Override + public Address[] getReplyTo() throws MessagingException { + Address[] a = getAddressHeader("Reply-To"); + if (a == null || a.length == 0) + a = getFrom(); + return a; + } + + /** + * Set the RFC 822 "Reply-To" header field. If the address + * parameter is null, this header is removed. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setReplyTo(Address[] addresses) throws MessagingException { + setAddressHeader("Reply-To", addresses); + } + + // Convenience method to get addresses + private Address[] getAddressHeader(String name) + throws MessagingException { + String s = getHeader(name, ","); + return (s == null) ? null : InternetAddress.parseHeader(s, strict); + } + + // Convenience method to set addresses + private void setAddressHeader(String name, Address[] addresses) + throws MessagingException { + String s; + if (allowutf8) + s = InternetAddress.toUnicodeString(addresses, name.length() + 2); + else + s = InternetAddress.toString(addresses, name.length() + 2); + if (s == null) + removeHeader(name); + else + setHeader(name, s); + } + + private void addAddressHeader(String name, Address[] addresses) + throws MessagingException { + if (addresses == null || addresses.length == 0) + return; + Address[] a = getAddressHeader(name); + Address[] anew; + if (a == null || a.length == 0) + anew = addresses; + else { + anew = new Address[a.length + addresses.length]; + System.arraycopy(a, 0, anew, 0, a.length); + System.arraycopy(addresses, 0, anew, a.length, addresses.length); + } + String s; + if (allowutf8) + s = InternetAddress.toUnicodeString(anew, name.length() + 2); + else + s = InternetAddress.toString(anew, name.length() + 2); + if (s == null) + return; + setHeader(name, s); + } + + /** + * Returns the value of the "Subject" header field. Returns null + * if the subject field is unavailable or its value is absent.

+ * + * If the subject is encoded as per RFC 2047, it is decoded and + * converted into Unicode. If the decoding or conversion fails, the + * raw data is returned as is.

+ * + * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return Subject + * @exception MessagingException for failures + * @see #headers + */ + @Override + public String getSubject() throws MessagingException { + String rawvalue = getHeader("Subject", null); + + if (rawvalue == null) + return null; + + try { + return MimeUtility.decodeText(MimeUtility.unfold(rawvalue)); + } catch (UnsupportedEncodingException ex) { + return rawvalue; + } + } + + /** + * Set the "Subject" header field. If the subject contains + * non US-ASCII characters, it will be encoded using the + * platform's default charset. If the subject contains only + * US-ASCII characters, no encoding is done and it is used + * as-is. If the subject is null, the existing "Subject" field + * is removed.

+ * + * The application must ensure that the subject does not contain + * any line breaks.

+ * + * Note that if the charset encoding process fails, a + * MessagingException is thrown, and an UnsupportedEncodingException + * is included in the chain of nested exceptions within the + * MessagingException. + * + * @param subject The subject + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setSubject(String subject) throws MessagingException { + setSubject(subject, null); + } + + /** + * Set the "Subject" header field. If the subject contains non + * US-ASCII characters, it will be encoded using the specified + * charset. If the subject contains only US-ASCII characters, no + * encoding is done and it is used as-is. If the subject is null, + * the existing "Subject" header field is removed.

+ * + * The application must ensure that the subject does not contain + * any line breaks.

+ * + * Note that if the charset encoding process fails, a + * MessagingException is thrown, and an UnsupportedEncodingException + * is included in the chain of nested exceptions within the + * MessagingException. + * + * @param subject The subject + * @param charset The charset + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + public void setSubject(String subject, String charset) + throws MessagingException { + if (subject == null) { + removeHeader("Subject"); + } else { + try { + setHeader("Subject", MimeUtility.fold(9, + MimeUtility.encodeText(subject, charset, null))); + } catch (UnsupportedEncodingException uex) { + throw new MessagingException("Encoding error", uex); + } + } + } + + /** + * Returns the value of the RFC 822 "Date" field. This is the date + * on which this message was sent. Returns null if this field is + * unavailable or its value is absent.

+ * + * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return The sent Date + * @exception MessagingException for failures + */ + @Override + public Date getSentDate() throws MessagingException { + String s = getHeader("Date", null); + if (s != null) { + try { + synchronized (mailDateFormat) { + return mailDateFormat.parse(s); + } + } catch (ParseException pex) { + return null; + } + } + + return null; + } + + /** + * Set the RFC 822 "Date" header field. This is the date on which the + * creator of the message indicates that the message is complete + * and ready for delivery. If the date parameter is + * null, the existing "Date" field is removed. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setSentDate(Date d) throws MessagingException { + if (d == null) + removeHeader("Date"); + else { + synchronized (mailDateFormat) { + setHeader("Date", mailDateFormat.format(d)); + } + } + } + + /** + * Returns the Date on this message was received. Returns + * null if this date cannot be obtained.

+ * + * Note that RFC 822 does not define a field for the received + * date. Hence only implementations that can provide this date + * need return a valid value.

+ * + * This implementation returns null. + * + * @return the date this message was received + * @exception MessagingException for failures + */ + @Override + public Date getReceivedDate() throws MessagingException { + return null; + } + + /** + * Return the size of the content of this message in bytes. + * Return -1 if the size cannot be determined.

+ * + * Note that this number may not be an exact measure of the + * content size and may or may not account for any transfer + * encoding of the content.

+ * + * This implementation returns the size of the content + * array (if not null), or, if contentStream is not + * null, and the available method returns a positive + * number, it returns that number as the size. Otherwise, it returns + * -1. + * + * @return size of content in bytes + * @exception MessagingException for failures + */ + @Override + public int getSize() throws MessagingException { + if (content != null) + return content.length; + if (contentStream != null) { + try { + int size = contentStream.available(); + // only believe the size if it's greater than zero, since zero + // is the default returned by the InputStream class itself + if (size > 0) + return size; + } catch (IOException ex) { + // ignore it + } + } + return -1; + } + + /** + * Return the number of lines for the content of this message. + * Return -1 if this number cannot be determined.

+ * + * Note that this number may not be an exact measure of the + * content length and may or may not account for any transfer + * encoding of the content.

+ * + * This implementation returns -1. + * + * @return number of lines in the content. + * @exception MessagingException for failures + */ + @Override + public int getLineCount() throws MessagingException { + return -1; + } + + /** + * Returns the value of the RFC 822 "Content-Type" header field. + * This represents the content-type of the content of this + * message. This value must not be null. If this field is + * unavailable, "text/plain" should be returned.

+ * + * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return The ContentType of this part + * @exception MessagingException for failures + * @see javax.activation.DataHandler + */ + @Override + public String getContentType() throws MessagingException { + String s = getHeader("Content-Type", null); + s = MimeUtil.cleanContentType(this, s); + if (s == null) + return "text/plain"; + return s; + } + + /** + * Is this Part of the specified MIME type? This method + * compares only the primaryType and + * subType. + * The parameters of the content types are ignored.

+ * + * For example, this method will return true when + * comparing a Part of content type "text/plain" + * with "text/plain; charset=foobar".

+ * + * If the subType of mimeType is the + * special character '*', then the subtype is ignored during the + * comparison. + * + * @param mimeType the MIME type to check + * @return true if it matches the MIME type + * @exception MessagingException for failures + */ + @Override + public boolean isMimeType(String mimeType) throws MessagingException { + return MimeBodyPart.isMimeType(this, mimeType); + } + + /** + * Returns the disposition from the "Content-Disposition" header field. + * This represents the disposition of this part. The disposition + * describes how the part should be presented to the user.

+ * + * If the Content-Disposition field is unavailable, + * null is returned.

+ * + * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return disposition of this part, or null if unknown + * @exception MessagingException for failures + */ + @Override + public String getDisposition() throws MessagingException { + return MimeBodyPart.getDisposition(this); + } + + /** + * Set the disposition in the "Content-Disposition" header field + * of this body part. If the disposition is null, any existing + * "Content-Disposition" header field is removed. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setDisposition(String disposition) throws MessagingException { + MimeBodyPart.setDisposition(this, disposition); + } + + /** + * Returns the content transfer encoding from the + * "Content-Transfer-Encoding" header + * field. Returns null if the header is unavailable + * or its value is absent.

+ * + * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return content-transfer-encoding + * @exception MessagingException for failures + */ + @Override + public String getEncoding() throws MessagingException { + return MimeBodyPart.getEncoding(this); + } + + /** + * Returns the value of the "Content-ID" header field. Returns + * null if the field is unavailable or its value is + * absent.

+ * + * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return content-ID + * @exception MessagingException for failures + */ + @Override + public String getContentID() throws MessagingException { + return getHeader("Content-Id", null); + } + + /** + * Set the "Content-ID" header field of this Message. + * If the cid parameter is null, any existing + * "Content-ID" is removed. + * + * @param cid the content ID + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + public void setContentID(String cid) throws MessagingException { + if (cid == null) + removeHeader("Content-ID"); + else + setHeader("Content-ID", cid); + } + + /** + * Return the value of the "Content-MD5" header field. Returns + * null if this field is unavailable or its value + * is absent.

+ * + * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return content-MD5 + * @exception MessagingException for failures + */ + @Override + public String getContentMD5() throws MessagingException { + return getHeader("Content-MD5", null); + } + + /** + * Set the "Content-MD5" header field of this Message. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setContentMD5(String md5) throws MessagingException { + setHeader("Content-MD5", md5); + } + + /** + * Returns the "Content-Description" header field of this Message. + * This typically associates some descriptive information with + * this part. Returns null if this field is unavailable or its + * value is absent.

+ * + * If the Content-Description field is encoded as per RFC 2047, + * it is decoded and converted into Unicode. If the decoding or + * conversion fails, the raw data is returned as-is

+ * + * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return content-description + * @exception MessagingException for failures + */ + @Override + public String getDescription() throws MessagingException { + return MimeBodyPart.getDescription(this); + } + + /** + * Set the "Content-Description" header field for this Message. + * If the description parameter is null, then any + * existing "Content-Description" fields are removed.

+ * + * If the description contains non US-ASCII characters, it will + * be encoded using the platform's default charset. If the + * description contains only US-ASCII characters, no encoding + * is done and it is used as-is.

+ * + * Note that if the charset encoding process fails, a + * MessagingException is thrown, and an UnsupportedEncodingException + * is included in the chain of nested exceptions within the + * MessagingException. + * + * @param description content-description + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException An + * UnsupportedEncodingException may be included + * in the exception chain if the charset + * conversion fails. + */ + @Override + public void setDescription(String description) throws MessagingException { + setDescription(description, null); + } + + /** + * Set the "Content-Description" header field for this Message. + * If the description parameter is null, then any + * existing "Content-Description" fields are removed.

+ * + * If the description contains non US-ASCII characters, it will + * be encoded using the specified charset. If the description + * contains only US-ASCII characters, no encoding is done and + * it is used as-is.

+ * + * Note that if the charset encoding process fails, a + * MessagingException is thrown, and an UnsupportedEncodingException + * is included in the chain of nested exceptions within the + * MessagingException. + * + * @param description Description + * @param charset Charset for encoding + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException An + * UnsupportedEncodingException may be included + * in the exception chain if the charset + * conversion fails. + */ + public void setDescription(String description, String charset) + throws MessagingException { + MimeBodyPart.setDescription(this, description, charset); + } + + /** + * Get the languages specified in the "Content-Language" header + * field of this message. The Content-Language header is defined by + * RFC 1766. Returns null if this field is unavailable + * or its value is absent.

+ * + * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return value of content-language header. + * @exception MessagingException for failures + */ + @Override + public String[] getContentLanguage() throws MessagingException { + return MimeBodyPart.getContentLanguage(this); + } + + /** + * Set the "Content-Language" header of this MimePart. The + * Content-Language header is defined by RFC 1766. + * + * @param languages array of language tags + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setContentLanguage(String[] languages) + throws MessagingException { + MimeBodyPart.setContentLanguage(this, languages); + } + + /** + * Returns the value of the "Message-ID" header field. Returns + * null if this field is unavailable or its value is absent.

+ * + * The default implementation provided here uses the + * getHeader method to return the value of the + * "Message-ID" field. + * + * @return Message-ID + * @exception MessagingException if the retrieval of this field + * causes any exception. + * @see javax.mail.search.MessageIDTerm + * @since JavaMail 1.1 + */ + public String getMessageID() throws MessagingException { + return getHeader("Message-ID", null); + } + + /** + * Get the filename associated with this Message.

+ * + * Returns the value of the "filename" parameter from the + * "Content-Disposition" header field of this message. If it's + * not available, returns the value of the "name" parameter from + * the "Content-Type" header field of this BodyPart. + * Returns null if both are absent.

+ * + * If the mail.mime.encodefilename System property + * is set to true, the {@link MimeUtility#decodeText + * MimeUtility.decodeText} method will be used to decode the + * filename. While such encoding is not supported by the MIME + * spec, many mailers use this technique to support non-ASCII + * characters in filenames. The default value of this property + * is false. + * + * @return filename + * @exception MessagingException for failures + */ + @Override + public String getFileName() throws MessagingException { + return MimeBodyPart.getFileName(this); + } + + /** + * Set the filename associated with this part, if possible.

+ * + * Sets the "filename" parameter of the "Content-Disposition" + * header field of this message.

+ * + * If the mail.mime.encodefilename System property + * is set to true, the {@link MimeUtility#encodeText + * MimeUtility.encodeText} method will be used to encode the + * filename. While such encoding is not supported by the MIME + * spec, many mailers use this technique to support non-ASCII + * characters in filenames. The default value of this property + * is false. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setFileName(String filename) throws MessagingException { + MimeBodyPart.setFileName(this, filename); + } + + private String getHeaderName(Message.RecipientType type) + throws MessagingException { + String headerName; + + if (type == Message.RecipientType.TO) + headerName = "To"; + else if (type == Message.RecipientType.CC) + headerName = "Cc"; + else if (type == Message.RecipientType.BCC) + headerName = "Bcc"; + else if (type == MimeMessage.RecipientType.NEWSGROUPS) + headerName = "Newsgroups"; + else + throw new MessagingException("Invalid Recipient Type"); + return headerName; + } + + + /** + * Return a decoded input stream for this Message's "content".

+ * + * This implementation obtains the input stream from the DataHandler, + * that is, it invokes getDataHandler().getInputStream(). + * + * @return an InputStream + * @exception IOException this is typically thrown by the + * DataHandler. Refer to the documentation for + * javax.activation.DataHandler for more details. + * @exception MessagingException for other failures + * + * @see #getContentStream + * @see javax.activation.DataHandler#getInputStream + */ + @Override + public InputStream getInputStream() + throws IOException, MessagingException { + return getDataHandler().getInputStream(); + } + + /** + * Produce the raw bytes of the content. This method is used during + * parsing, to create a DataHandler object for the content. Subclasses + * that can provide a separate input stream for just the message + * content might want to override this method.

+ * + * This implementation returns a SharedInputStream, if + * contentStream is not null. Otherwise, it + * returns a ByteArrayInputStream constructed + * out of the content byte array. + * + * @return an InputStream containing the raw bytes + * @exception MessagingException for failures + * @see #content + */ + protected InputStream getContentStream() throws MessagingException { + if (contentStream != null) + return ((SharedInputStream)contentStream).newStream(0, -1); + if (content != null) + return new SharedByteArrayInputStream(content); + + throw new MessagingException("No MimeMessage content"); + } + + /** + * Return an InputStream to the raw data with any Content-Transfer-Encoding + * intact. This method is useful if the "Content-Transfer-Encoding" + * header is incorrect or corrupt, which would prevent the + * getInputStream method or getContent method + * from returning the correct data. In such a case the application may + * use this method and attempt to decode the raw data itself.

+ * + * This implementation simply calls the getContentStream + * method. + * + * @return an InputStream containing the raw bytes + * @exception MessagingException for failures + * @see #getInputStream + * @see #getContentStream + * @since JavaMail 1.2 + */ + public InputStream getRawInputStream() throws MessagingException { + return getContentStream(); + } + + /** + * Return a DataHandler for this Message's content.

+ * + * The implementation provided here works approximately as follows. + * Note the use of the getContentStream method to + * generate the byte stream for the content. Also note that + * any transfer-decoding is done automatically within this method. + * + *

+     *  getDataHandler() {
+     *      if (dh == null) {
+     *          dh = new DataHandler(new MimePartDataSource(this));
+     *      }
+     *      return dh;
+     *  }
+     *
+     *  class MimePartDataSource implements DataSource {
+     *      public getInputStream() {
+     *          return MimeUtility.decode(
+     *		     getContentStream(), getEncoding());
+     *      }
+     *	
+     *		.... <other DataSource methods>
+     *  }
+     * 

+ * + * @exception MessagingException for failures + */ + @Override + public synchronized DataHandler getDataHandler() + throws MessagingException { + if (dh == null) + dh = new MimeBodyPart.MimePartDataHandler(this); + return dh; + } + + /** + * Return the content as a Java object. The type of this + * object is dependent on the content itself. For + * example, the native format of a "text/plain" content + * is usually a String object. The native format for a "multipart" + * message is always a Multipart subclass. For content types that are + * unknown to the DataHandler system, an input stream is returned + * as the content.

+ * + * This implementation obtains the content from the DataHandler, + * that is, it invokes getDataHandler().getContent(). + * If the content is a Multipart or Message object and was created by + * parsing a stream, the object is cached and returned in subsequent + * calls so that modifications to the content will not be lost. + * + * @return Object + * @see javax.mail.Part + * @see javax.activation.DataHandler#getContent + * @exception IOException this is typically thrown by the + * DataHandler. Refer to the documentation for + * javax.activation.DataHandler for more details. + * @exception MessagingException for other failures + */ + @Override + public Object getContent() throws IOException, MessagingException { + if (cachedContent != null) + return cachedContent; + Object c; + try { + c = getDataHandler().getContent(); + } catch (FolderClosedIOException fex) { + throw new FolderClosedException(fex.getFolder(), fex.getMessage()); + } catch (MessageRemovedIOException mex) { + throw new MessageRemovedException(mex.getMessage()); + } + if (MimeBodyPart.cacheMultipart && + (c instanceof Multipart || c instanceof Message) && + (content != null || contentStream != null)) { + cachedContent = c; + /* + * We may abandon the input stream so make sure + * the MimeMultipart has consumed the stream. + */ + if (c instanceof MimeMultipart) + ((MimeMultipart)c).parse(); + } + return c; + } + + /** + * This method provides the mechanism to set this part's content. + * The given DataHandler object should wrap the actual content. + * + * @param dh The DataHandler for the content. + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public synchronized void setDataHandler(DataHandler dh) + throws MessagingException { + this.dh = dh; + cachedContent = null; + MimeBodyPart.invalidateContentHeaders(this); + } + + /** + * A convenience method for setting this Message's content.

+ * + * The content is wrapped in a DataHandler object. Note that a + * DataContentHandler class for the specified type should be + * available to the JavaMail implementation for this to work right. + * i.e., to do setContent(foobar, "application/x-foobar"), + * a DataContentHandler for "application/x-foobar" should be installed. + * Refer to the Java Activation Framework for more information. + * + * @param o the content object + * @param type Mime type of the object + * @exception IllegalWriteException if the underlying + * implementation does not support modification of + * existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setContent(Object o, String type) + throws MessagingException { + if (o instanceof Multipart) + setContent((Multipart)o); + else + setDataHandler(new DataHandler(o, type)); + } + + /** + * Convenience method that sets the given String as this + * part's content, with a MIME type of "text/plain". If the + * string contains non US-ASCII characters. it will be encoded + * using the platform's default charset. The charset is also + * used to set the "charset" parameter.

+ * + * Note that there may be a performance penalty if + * text is large, since this method may have + * to scan all the characters to determine what charset to + * use.

+ * + * If the charset is already known, use the + * setText method that takes the charset parameter. + * + * @param text the text content to set + * @exception MessagingException if an error occurs + * @see #setText(String text, String charset) + */ + @Override + public void setText(String text) throws MessagingException { + setText(text, null); + } + + /** + * Convenience method that sets the given String as this part's + * content, with a MIME type of "text/plain" and the specified + * charset. The given Unicode string will be charset-encoded + * using the specified charset. The charset is also used to set + * the "charset" parameter. + * + * @param text the text content to set + * @param charset the charset to use for the text + * @exception MessagingException if an error occurs + */ + @Override + public void setText(String text, String charset) + throws MessagingException { + MimeBodyPart.setText(this, text, charset, "plain"); + } + + /** + * Convenience method that sets the given String as this part's + * content, with a primary MIME type of "text" and the specified + * MIME subtype. The given Unicode string will be charset-encoded + * using the specified charset. The charset is also used to set + * the "charset" parameter. + * + * @param text the text content to set + * @param charset the charset to use for the text + * @param subtype the MIME subtype to use (e.g., "html") + * @exception MessagingException if an error occurs + * @since JavaMail 1.4 + */ + @Override + public void setText(String text, String charset, String subtype) + throws MessagingException { + MimeBodyPart.setText(this, text, charset, subtype); + } + + /** + * This method sets the Message's content to a Multipart object. + * + * @param mp The multipart object that is the Message's content + * @exception IllegalWriteException if the underlying + * implementation does not support modification of + * existing values + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setContent(Multipart mp) throws MessagingException { + setDataHandler(new DataHandler(mp, mp.getContentType())); + mp.setParent(this); + } + + /** + * Get a new Message suitable for a reply to this message. + * The new Message will have its attributes and headers + * set up appropriately. Note that this new message object + * will be empty, i.e., it will not have a "content". + * These will have to be suitably filled in by the client.

+ * + * If replyToAll is set, the new Message will be addressed + * to all recipients of this message. Otherwise, the reply will be + * addressed to only the sender of this message (using the value + * of the getReplyTo method).

+ * + * The "Subject" field is filled in with the original subject + * prefixed with "Re:" (unless it already starts with "Re:"). + * The "In-Reply-To" header is set in the new message if this + * message has a "Message-Id" header. The ANSWERED + * flag is set in this message. + * + * The current implementation also sets the "References" header + * in the new message to include the contents of the "References" + * header (or, if missing, the "In-Reply-To" header) in this message, + * plus the contents of the "Message-Id" header of this message, + * as described in RFC 2822. + * + * @param replyToAll reply should be sent to all recipients + * of this message + * @return the reply Message + * @exception MessagingException for failures + */ + @Override + public Message reply(boolean replyToAll) throws MessagingException { + return reply(replyToAll, true); + } + + /** + * Get a new Message suitable for a reply to this message. + * The new Message will have its attributes and headers + * set up appropriately. Note that this new message object + * will be empty, i.e., it will not have a "content". + * These will have to be suitably filled in by the client.

+ * + * If replyToAll is set, the new Message will be addressed + * to all recipients of this message. Otherwise, the reply will be + * addressed to only the sender of this message (using the value + * of the getReplyTo method).

+ * + * If setAnswered is set, the + * {@link javax.mail.Flags.Flag#ANSWERED ANSWERED} flag is set + * in this message.

+ * + * The "Subject" field is filled in with the original subject + * prefixed with "Re:" (unless it already starts with "Re:"). + * The "In-Reply-To" header is set in the new message if this + * message has a "Message-Id" header. + * + * The current implementation also sets the "References" header + * in the new message to include the contents of the "References" + * header (or, if missing, the "In-Reply-To" header) in this message, + * plus the contents of the "Message-Id" header of this message, + * as described in RFC 2822. + * + * @param replyToAll reply should be sent to all recipients + * of this message + * @param setAnswered set the ANSWERED flag in this message? + * @return the reply Message + * @exception MessagingException for failures + * @since JavaMail 1.5 + */ + public Message reply(boolean replyToAll, boolean setAnswered) + throws MessagingException { + MimeMessage reply = createMimeMessage(session); + /* + * Have to manipulate the raw Subject header so that we don't lose + * any encoding information. This is safe because "Re:" isn't + * internationalized and (generally) isn't encoded. If the entire + * Subject header is encoded, prefixing it with "Re: " still leaves + * a valid and correct encoded header. + */ + String subject = getHeader("Subject", null); + if (subject != null) { + if (!subject.regionMatches(true, 0, "Re: ", 0, 4)) + subject = "Re: " + subject; + reply.setHeader("Subject", subject); + } + Address a[] = getReplyTo(); + reply.setRecipients(Message.RecipientType.TO, a); + if (replyToAll) { + List

v = new ArrayList<>(); + // add my own address to list + InternetAddress me = InternetAddress.getLocalAddress(session); + if (me != null) + v.add(me); + // add any alternate names I'm known by + String alternates = null; + if (session != null) + alternates = session.getProperty("mail.alternates"); + if (alternates != null) + eliminateDuplicates(v, + InternetAddress.parse(alternates, false)); + // should we Cc all other original recipients? + String replyallccStr = null; + boolean replyallcc = false; + if (session != null) + replyallcc = PropUtil.getBooleanProperty( + session.getProperties(), + "mail.replyallcc", false); + // add the recipients from the To field so far + eliminateDuplicates(v, a); + a = getRecipients(Message.RecipientType.TO); + a = eliminateDuplicates(v, a); + if (a != null && a.length > 0) { + if (replyallcc) + reply.addRecipients(Message.RecipientType.CC, a); + else + reply.addRecipients(Message.RecipientType.TO, a); + } + a = getRecipients(Message.RecipientType.CC); + a = eliminateDuplicates(v, a); + if (a != null && a.length > 0) + reply.addRecipients(Message.RecipientType.CC, a); + // don't eliminate duplicate newsgroups + a = getRecipients(RecipientType.NEWSGROUPS); + if (a != null && a.length > 0) + reply.setRecipients(RecipientType.NEWSGROUPS, a); + } + + String msgId = getHeader("Message-Id", null); + if (msgId != null) + reply.setHeader("In-Reply-To", msgId); + + /* + * Set the References header as described in RFC 2822: + * + * The "References:" field will contain the contents of the parent's + * "References:" field (if any) followed by the contents of the parent's + * "Message-ID:" field (if any). If the parent message does not contain + * a "References:" field but does have an "In-Reply-To:" field + * containing a single message identifier, then the "References:" field + * will contain the contents of the parent's "In-Reply-To:" field + * followed by the contents of the parent's "Message-ID:" field (if + * any). If the parent has none of the "References:", "In-Reply-To:", + * or "Message-ID:" fields, then the new message will have no + * "References:" field. + */ + String refs = getHeader("References", " "); + if (refs == null) { + // XXX - should only use if it contains a single message identifier + refs = getHeader("In-Reply-To", " "); + } + if (msgId != null) { + if (refs != null) + refs = MimeUtility.unfold(refs) + " " + msgId; + else + refs = msgId; + } + if (refs != null) + reply.setHeader("References", MimeUtility.fold(12, refs)); + + if (setAnswered) { + try { + setFlags(answeredFlag, true); + } catch (MessagingException mex) { + // ignore it + } + } + return reply; + } + + // used above in reply() + private static final Flags answeredFlag = new Flags(Flags.Flag.ANSWERED); + + /** + * Check addrs for any duplicates that may already be in v. + * Return a new array without the duplicates. Add any new + * addresses to v. Note that the input array may be modified. + */ + private Address[] eliminateDuplicates(List
v, Address[] addrs) { + if (addrs == null) + return null; + int gone = 0; + for (int i = 0; i < addrs.length; i++) { + boolean found = false; + // search the list for this address + for (int j = 0; j < v.size(); j++) { + if (((InternetAddress)v.get(j)).equals(addrs[i])) { + // found it; count it and remove it from the input array + found = true; + gone++; + addrs[i] = null; + break; + } + } + if (!found) + v.add(addrs[i]); // add new address to list + } + // if we found any duplicates, squish the array + if (gone != 0) { + Address[] a; + // new array should be same type as original array + // XXX - there must be a better way, perhaps reflection? + if (addrs instanceof InternetAddress[]) + a = new InternetAddress[addrs.length - gone]; + else + a = new Address[addrs.length - gone]; + for (int i = 0, j = 0; i < addrs.length; i++) + if (addrs[i] != null) + a[j++] = addrs[i]; + addrs = a; + } + return addrs; + } + + /** + * Output the message as an RFC 822 format stream.

+ * + * Note that, depending on how the messag was constructed, it may + * use a variety of line termination conventions. Generally the + * output should be sent through an appropriate FilterOutputStream + * that converts the line terminators to the desired form, either + * CRLF for MIME compatibility and for use in Internet protocols, + * or the local platform's line terminator for storage in a local + * text file.

+ * + * This implementation calls the writeTo(OutputStream, + * String[]) method with a null ignore list. + * + * @exception IOException if an error occurs writing to the stream + * or if an error is generated by the + * javax.activation layer. + * @exception MessagingException for other failures + * @see javax.activation.DataHandler#writeTo + */ + @Override + public void writeTo(OutputStream os) + throws IOException, MessagingException { + writeTo(os, null); + } + + /** + * Output the message as an RFC 822 format stream, without + * specified headers. If the saved flag is not set, + * the saveChanges method is called. + * If the modified flag is not + * set and the content array is not null, the + * content array is written directly, after + * writing the appropriate message headers. + * + * @param os the stream to write to + * @param ignoreList the headers to not include in the output + * @exception IOException if an error occurs writing to the stream + * or if an error is generated by the + * javax.activation layer. + * @exception javax.mail.MessagingException for other failures + * @see javax.activation.DataHandler#writeTo + */ + public void writeTo(OutputStream os, String[] ignoreList) + throws IOException, MessagingException { + if (!saved) + saveChanges(); + + if (modified) { + MimeBodyPart.writeTo(this, os, ignoreList); + return; + } + + // Else, the content is untouched, so we can just output it + // First, write out the header + Enumeration hdrLines = getNonMatchingHeaderLines(ignoreList); + LineOutputStream los = new LineOutputStream(os, allowutf8); + while (hdrLines.hasMoreElements()) + los.writeln(hdrLines.nextElement()); + + // The CRLF separator between header and content + los.writeln(); + + // Finally, the content. + if (content == null) { + // call getContentStream to give subclass a chance to + // provide the data on demand + InputStream is = null; + byte[] buf = new byte[8192]; + try { + is = getContentStream(); + // now copy the data to the output stream + int len; + while ((len = is.read(buf)) > 0) + os.write(buf, 0, len); + } finally { + if (is != null) + is.close(); + buf = null; + } + } else { + os.write(content); + } + os.flush(); + } + + /** + * Get all the headers for this header_name. Note that certain + * headers may be encoded as per RFC 2047 if they contain + * non US-ASCII characters and these should be decoded.

+ * + * This implementation obtains the headers from the + * headers InternetHeaders object. + * + * @param name name of header + * @return array of headers + * @exception MessagingException for failures + * @see javax.mail.internet.MimeUtility + */ + @Override + public String[] getHeader(String name) + throws MessagingException { + return headers.getHeader(name); + } + + /** + * Get all the headers for this header name, returned as a single + * String, with headers separated by the delimiter. If the + * delimiter is null, only the first header is + * returned. + * + * @param name the name of this header + * @param delimiter separator between values + * @return the value fields for all headers with + * this name + * @exception MessagingException for failures + */ + @Override + public String getHeader(String name, String delimiter) + throws MessagingException { + return headers.getHeader(name, delimiter); + } + + /** + * Set the value for this header_name. Replaces all existing + * header values with this new value. Note that RFC 822 headers + * must contain only US-ASCII characters, so a header that + * contains non US-ASCII characters must have been encoded by the + * caller as per the rules of RFC 2047. + * + * @param name header name + * @param value header value + * @see javax.mail.internet.MimeUtility + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setHeader(String name, String value) + throws MessagingException { + headers.setHeader(name, value); + } + + /** + * Add this value to the existing values for this header_name. + * Note that RFC 822 headers must contain only US-ASCII + * characters, so a header that contains non US-ASCII characters + * must have been encoded as per the rules of RFC 2047. + * + * @param name header name + * @param value header value + * @see javax.mail.internet.MimeUtility + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void addHeader(String name, String value) + throws MessagingException { + headers.addHeader(name, value); + } + + /** + * Remove all headers with this name. + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void removeHeader(String name) + throws MessagingException { + headers.removeHeader(name); + } + + /** + * Return all the headers from this Message as an enumeration + * of Header objects.

+ * + * Note that certain headers may be encoded as per RFC 2047 + * if they contain non US-ASCII characters and these should + * be decoded.

+ * + * This implementation obtains the headers from the + * headers InternetHeaders object. + * + * @return array of header objects + * @exception MessagingException for failures + * @see javax.mail.internet.MimeUtility + */ + @Override + public Enumeration

getAllHeaders() throws MessagingException { + return headers.getAllHeaders(); + } + + /** + * Return matching headers from this Message as an Enumeration of + * Header objects. This implementation obtains the headers from + * the headers InternetHeaders object. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration
getMatchingHeaders(String[] names) + throws MessagingException { + return headers.getMatchingHeaders(names); + } + + /** + * Return non-matching headers from this Message as an + * Enumeration of Header objects. This implementation + * obtains the header from the headers InternetHeaders object. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration
getNonMatchingHeaders(String[] names) + throws MessagingException { + return headers.getNonMatchingHeaders(names); + } + + /** + * Add a raw RFC 822 header-line. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void addHeaderLine(String line) throws MessagingException { + headers.addHeaderLine(line); + } + + /** + * Get all header lines as an Enumeration of Strings. A Header + * line is a raw RFC 822 header-line, containing both the "name" + * and "value" field. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration getAllHeaderLines() throws MessagingException { + return headers.getAllHeaderLines(); + } + + /** + * Get matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC 822 header-line, containing both + * the "name" and "value" field. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration getMatchingHeaderLines(String[] names) + throws MessagingException { + return headers.getMatchingHeaderLines(names); + } + + /** + * Get non-matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC 822 header-line, containing both + * the "name" and "value" field. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration getNonMatchingHeaderLines(String[] names) + throws MessagingException { + return headers.getNonMatchingHeaderLines(names); + } + + /** + * Return a Flags object containing the flags for + * this message.

+ * + * Note that a clone of the internal Flags object is returned, so + * modifying the returned Flags object will not affect the flags + * of this message. + * + * @return Flags object containing the flags for this message + * @exception MessagingException for failures + * @see javax.mail.Flags + */ + @Override + public synchronized Flags getFlags() throws MessagingException { + return (Flags)flags.clone(); + } + + /** + * Check whether the flag specified in the flag + * argument is set in this message.

+ * + * This implementation checks this message's internal + * flags object. + * + * @param flag the flag + * @return value of the specified flag for this message + * @exception MessagingException for failures + * @see javax.mail.Flags.Flag + * @see javax.mail.Flags.Flag#ANSWERED + * @see javax.mail.Flags.Flag#DELETED + * @see javax.mail.Flags.Flag#DRAFT + * @see javax.mail.Flags.Flag#FLAGGED + * @see javax.mail.Flags.Flag#RECENT + * @see javax.mail.Flags.Flag#SEEN + */ + @Override + public synchronized boolean isSet(Flags.Flag flag) + throws MessagingException { + return (flags.contains(flag)); + } + + /** + * Set the flags for this message.

+ * + * This implementation modifies the flags field. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public synchronized void setFlags(Flags flag, boolean set) + throws MessagingException { + if (set) + flags.add(flag); + else + flags.remove(flag); + } + + /** + * Updates the appropriate header fields of this message to be + * consistent with the message's contents. If this message is + * contained in a Folder, any changes made to this message are + * committed to the containing folder.

+ * + * If any part of a message's headers or contents are changed, + * saveChanges must be called to ensure that those + * changes are permanent. Otherwise, any such modifications may or + * may not be saved, depending on the folder implementation.

+ * + * Messages obtained from folders opened READ_ONLY should not be + * modified and saveChanges should not be called on such messages.

+ * + * This method sets the modified flag to true, the + * save flag to true, and then calls the + * updateHeaders method. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void saveChanges() throws MessagingException { + modified = true; + saved = true; + updateHeaders(); + } + + /** + * Update the Message-ID header. This method is called + * by the updateHeaders and allows a subclass + * to override only the algorithm for choosing a Message-ID. + * + * @exception MessagingException for failures + * @since JavaMail 1.4 + */ + protected void updateMessageID() throws MessagingException { + setHeader("Message-ID", + "<" + UniqueValue.getUniqueMessageIDValue(session) + ">"); + + } + + /** + * Called by the saveChanges method to actually + * update the MIME headers. The implementation here sets the + * Content-Transfer-Encoding header (if needed + * and not already set), the Date header (if + * not already set), the MIME-Version header + * and the Message-ID header. Also, if the content + * of this message is a MimeMultipart, its + * updateHeaders method is called.

+ * + * If the {@link #cachedContent} field is not null (that is, + * it references a Multipart or Message object), then + * that object is used to set a new DataHandler, any + * stream data used to create this object is discarded, + * and the {@link #cachedContent} field is cleared. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + protected synchronized void updateHeaders() throws MessagingException { + MimeBodyPart.updateHeaders(this); + setHeader("MIME-Version", "1.0"); + if (getHeader("Date") == null) + setSentDate(new Date()); + updateMessageID(); + + if (cachedContent != null) { + dh = new DataHandler(cachedContent, getContentType()); + cachedContent = null; + content = null; + if (contentStream != null) { + try { + contentStream.close(); + } catch (IOException ioex) { } // nothing to do + } + contentStream = null; + } + } + + /** + * Create and return an InternetHeaders object that loads the + * headers from the given InputStream. Subclasses can override + * this method to return a subclass of InternetHeaders, if + * necessary. This implementation simply constructs and returns + * an InternetHeaders object. + * + * @return an InternetHeaders object + * @param is the InputStream to read the headers from + * @exception MessagingException for failures + * @since JavaMail 1.2 + */ + protected InternetHeaders createInternetHeaders(InputStream is) + throws MessagingException { + return new InternetHeaders(is, allowutf8); + } + + /** + * Create and return a MimeMessage object. The reply method + * uses this method to create the MimeMessage object that it + * will return. Subclasses can override this method to return + * a subclass of MimeMessage. This implementation simply constructs + * and returns a MimeMessage object using the supplied Session. + * + * @param session the Session to use for the new message + * @return the new MimeMessage object + * @exception MessagingException for failures + * @since JavaMail 1.4 + */ + protected MimeMessage createMimeMessage(Session session) + throws MessagingException { + return new MimeMessage(session); + } +} diff --git a/app/src/main/java/javax/mail/internet/MimeMultipart.java b/app/src/main/java/javax/mail/internet/MimeMultipart.java new file mode 100644 index 0000000000..2af69a7092 --- /dev/null +++ b/app/src/main/java/javax/mail/internet/MimeMultipart.java @@ -0,0 +1,1009 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import javax.mail.*; +import javax.activation.*; +import java.util.*; +import java.io.*; +import com.sun.mail.util.LineOutputStream; +import com.sun.mail.util.LineInputStream; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.util.PropUtil; + +/** + * The MimeMultipart class is an implementation of the abstract Multipart + * class that uses MIME conventions for the multipart data.

+ * + * A MimeMultipart is obtained from a MimePart whose primary type + * is "multipart" (by invoking the part's getContent() method) + * or it can be created by a client as part of creating a new MimeMessage.

+ * + * The default multipart subtype is "mixed". The other multipart + * subtypes, such as "alternative", "related", and so on, can be + * implemented as subclasses of MimeMultipart with additional methods + * to implement the additional semantics of that type of multipart + * content. The intent is that service providers, mail JavaBean writers + * and mail clients will write many such subclasses and their Command + * Beans, and will install them into the JavaBeans Activation + * Framework, so that any Jakarta Mail implementation and its clients can + * transparently find and use these classes. Thus, a MIME multipart + * handler is treated just like any other type handler, thereby + * decoupling the process of providing multipart handlers from the + * Jakarta Mail API. Lacking these additional MimeMultipart subclasses, + * all subtypes of MIME multipart data appear as MimeMultipart objects.

+ * + * An application can directly construct a MIME multipart object of any + * subtype by using the MimeMultipart(String subtype) + * constructor. For example, to create a "multipart/alternative" object, + * use new MimeMultipart("alternative").

+ * + * The mail.mime.multipart.ignoremissingendboundary + * property may be set to false to cause a + * MessagingException to be thrown if the multipart + * data does not end with the required end boundary line. If this + * property is set to true or not set, missing end + * boundaries are not considered an error and the final body part + * ends at the end of the data.

+ * + * The mail.mime.multipart.ignoremissingboundaryparameter + * System property may be set to false to cause a + * MessagingException to be thrown if the Content-Type + * of the MimeMultipart does not include a boundary parameter. + * If this property is set to true or not set, the multipart + * parsing code will look for a line that looks like a bounary line and + * use that as the boundary separating the parts.

+ * + * The mail.mime.multipart.ignoreexistingboundaryparameter + * System property may be set to true to cause any boundary + * to be ignored and instead search for a boundary line in the message + * as with mail.mime.multipart.ignoremissingboundaryparameter.

+ * + * Normally, when writing out a MimeMultipart that contains no body + * parts, or when trying to parse a multipart message with no body parts, + * a MessagingException is thrown. The MIME spec does not allow + * multipart content with no body parts. The + * mail.mime.multipart.allowempty System property may be set to + * true to override this behavior. + * When writing out such a MimeMultipart, a single empty part will be + * included. When reading such a multipart, a MimeMultipart will be created + * with no body parts. + * + * @author John Mani + * @author Bill Shannon + * @author Max Spivak + */ + +public class MimeMultipart extends Multipart { + + /** + * The DataSource supplying our InputStream. + */ + protected DataSource ds = null; + + /** + * Have we parsed the data from our InputStream yet? + * Defaults to true; set to false when our constructor is + * given a DataSource with an InputStream that we need to + * parse. + */ + protected boolean parsed = true; + + /** + * Have we seen the final bounary line? + * + * @since JavaMail 1.5 + */ + protected boolean complete = true; + + /** + * The MIME multipart preamble text, the text that + * occurs before the first boundary line. + * + * @since JavaMail 1.5 + */ + protected String preamble = null; + + /** + * Flag corresponding to the "mail.mime.multipart.ignoremissingendboundary" + * property, set in the {@link #initializeProperties} method called from + * constructors and the parse method. + * + * @since JavaMail 1.5 + */ + protected boolean ignoreMissingEndBoundary = true; + + /** + * Flag corresponding to the + * "mail.mime.multipart.ignoremissingboundaryparameter" + * property, set in the {@link #initializeProperties} method called from + * constructors and the parse method. + * + * @since JavaMail 1.5 + */ + protected boolean ignoreMissingBoundaryParameter = true; + + /** + * Flag corresponding to the + * "mail.mime.multipart.ignoreexistingboundaryparameter" + * property, set in the {@link #initializeProperties} method called from + * constructors and the parse method. + * + * @since JavaMail 1.5 + */ + protected boolean ignoreExistingBoundaryParameter = false; + + /** + * Flag corresponding to the "mail.mime.multipart.allowempty" + * property, set in the {@link #initializeProperties} method called from + * constructors and the parse method. + * + * @since JavaMail 1.5 + */ + protected boolean allowEmpty = false; + + /** + * Default constructor. An empty MimeMultipart object + * is created. Its content type is set to "multipart/mixed". + * A unique boundary string is generated and this string is + * setup as the "boundary" parameter for the + * contentType field.

+ * + * MimeBodyParts may be added later. + */ + public MimeMultipart() { + this("mixed"); + } + + /** + * Construct a MimeMultipart object of the given subtype. + * A unique boundary string is generated and this string is + * setup as the "boundary" parameter for the + * contentType field. + * Calls the {@link #initializeProperties} method.

+ * + * MimeBodyParts may be added later. + * + * @param subtype the MIME content subtype + */ + public MimeMultipart(String subtype) { + super(); + /* + * Compute a boundary string. + */ + String boundary = UniqueValue.getUniqueBoundaryValue(); + ContentType cType = new ContentType("multipart", subtype, null); + cType.setParameter("boundary", boundary); + contentType = cType.toString(); + initializeProperties(); + } + + /** + * Construct a MimeMultipart object of the default "mixed" subtype, + * and with the given body parts. More body parts may be added later. + * + * @param parts the body parts + * @exception MessagingException for failures + * @since JavaMail 1.5 + */ + public MimeMultipart(BodyPart... parts) throws MessagingException { + this(); + for (BodyPart bp : parts) + super.addBodyPart(bp); + } + + /** + * Construct a MimeMultipart object of the given subtype + * and with the given body parts. More body parts may be added later. + * + * @param subtype the MIME content subtype + * @param parts the body parts + * @exception MessagingException for failures + * @since JavaMail 1.5 + */ + public MimeMultipart(String subtype, BodyPart... parts) + throws MessagingException { + this(subtype); + for (BodyPart bp : parts) + super.addBodyPart(bp); + } + + /** + * Constructs a MimeMultipart object and its bodyparts from the + * given DataSource.

+ * + * This constructor handles as a special case the situation where the + * given DataSource is a MultipartDataSource object. In this case, this + * method just invokes the superclass (i.e., Multipart) constructor + * that takes a MultipartDataSource object.

+ * + * Otherwise, the DataSource is assumed to provide a MIME multipart + * byte stream. The parsed flag is set to false. When + * the data for the body parts are needed, the parser extracts the + * "boundary" parameter from the content type of this DataSource, + * skips the 'preamble' and reads bytes till the terminating + * boundary and creates MimeBodyParts for each part of the stream. + * + * @param ds DataSource, can be a MultipartDataSource + * @exception ParseException for failures parsing the message + * @exception MessagingException for other failures + */ + public MimeMultipart(DataSource ds) throws MessagingException { + super(); + + if (ds instanceof MessageAware) { + MessageContext mc = ((MessageAware)ds).getMessageContext(); + setParent(mc.getPart()); + } + + if (ds instanceof MultipartDataSource) { + // ask super to do this for us. + setMultipartDataSource((MultipartDataSource)ds); + return; + } + + // 'ds' was not a MultipartDataSource, we have + // to parse this ourself. + parsed = false; + this.ds = ds; + contentType = ds.getContentType(); + } + + /** + * Initialize flags that control parsing behavior, + * based on System properties described above in + * the class documentation. + * + * @since JavaMail 1.5 + */ + protected void initializeProperties() { + // read properties that control parsing + + // default to true + ignoreMissingEndBoundary = PropUtil.getBooleanSystemProperty( + "mail.mime.multipart.ignoremissingendboundary", true); + // default to true + ignoreMissingBoundaryParameter = PropUtil.getBooleanSystemProperty( + "mail.mime.multipart.ignoremissingboundaryparameter", true); + // default to false + ignoreExistingBoundaryParameter = PropUtil.getBooleanSystemProperty( + "mail.mime.multipart.ignoreexistingboundaryparameter", false); + // default to false + allowEmpty = PropUtil.getBooleanSystemProperty( + "mail.mime.multipart.allowempty", false); + } + + /** + * Set the subtype. This method should be invoked only on a new + * MimeMultipart object created by the client. The default subtype + * of such a multipart object is "mixed".

+ * + * @param subtype Subtype + * @exception MessagingException for failures + */ + public synchronized void setSubType(String subtype) + throws MessagingException { + ContentType cType = new ContentType(contentType); + cType.setSubType(subtype); + contentType = cType.toString(); + } + + /** + * Return the number of enclosed BodyPart objects. + * + * @return number of parts + */ + @Override + public synchronized int getCount() throws MessagingException { + parse(); + return super.getCount(); + } + + /** + * Get the specified BodyPart. BodyParts are numbered starting at 0. + * + * @param index the index of the desired BodyPart + * @return the Part + * @exception MessagingException if no such BodyPart exists + */ + @Override + public synchronized BodyPart getBodyPart(int index) + throws MessagingException { + parse(); + return super.getBodyPart(index); + } + + /** + * Get the MimeBodyPart referred to by the given ContentID (CID). + * Returns null if the part is not found. + * + * @param CID the ContentID of the desired part + * @return the Part + * @exception MessagingException for failures + */ + public synchronized BodyPart getBodyPart(String CID) + throws MessagingException { + parse(); + + int count = getCount(); + for (int i = 0; i < count; i++) { + MimeBodyPart part = (MimeBodyPart)getBodyPart(i); + String s = part.getContentID(); + if (s != null && s.equals(CID)) + return part; + } + return null; + } + + /** + * Remove the specified part from the multipart message. + * Shifts all the parts after the removed part down one. + * + * @param part The part to remove + * @return true if part removed, false otherwise + * @exception MessagingException if no such Part exists + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + */ + @Override + public boolean removeBodyPart(BodyPart part) throws MessagingException { + parse(); + return super.removeBodyPart(part); + } + + /** + * Remove the part at specified location (starting from 0). + * Shifts all the parts after the removed part down one. + * + * @param index Index of the part to remove + * @exception IndexOutOfBoundsException if the given index + * is out of range. + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception MessagingException for other failures + */ + @Override + public void removeBodyPart(int index) throws MessagingException { + parse(); + super.removeBodyPart(index); + } + + /** + * Adds a Part to the multipart. The BodyPart is appended to + * the list of existing Parts. + * + * @param part The Part to be appended + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception MessagingException for other failures + */ + @Override + public synchronized void addBodyPart(BodyPart part) + throws MessagingException { + parse(); + super.addBodyPart(part); + } + + /** + * Adds a BodyPart at position index. + * If index is not the last one in the list, + * the subsequent parts are shifted up. If index + * is larger than the number of parts present, the + * BodyPart is appended to the end. + * + * @param part The BodyPart to be inserted + * @param index Location where to insert the part + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @exception MessagingException for other failures + */ + @Override + public synchronized void addBodyPart(BodyPart part, int index) + throws MessagingException { + parse(); + super.addBodyPart(part, index); + } + + /** + * Return true if the final boundary line for this + * multipart was seen. When parsing multipart content, + * this class will (by default) terminate parsing with + * no error if the end of input is reached before seeing + * the final multipart boundary line. In such a case, + * this method will return false. (If the System property + * "mail.mime.multipart.ignoremissingendboundary" is set to + * false, parsing such a message will instead throw a + * MessagingException.) + * + * @return true if the final boundary line was seen + * @exception MessagingException for failures + * @since JavaMail 1.4 + */ + public synchronized boolean isComplete() throws MessagingException { + parse(); + return complete; + } + + /** + * Get the preamble text, if any, that appears before the + * first body part of this multipart. Some protocols, + * such as IMAP, will not allow access to the preamble text. + * + * @return the preamble text, or null if no preamble + * @exception MessagingException for failures + * @since JavaMail 1.4 + */ + public synchronized String getPreamble() throws MessagingException { + parse(); + return preamble; + } + + /** + * Set the preamble text to be included before the first + * body part. Applications should generally not include + * any preamble text. In some cases it may be helpful to + * include preamble text with instructions for users of + * pre-MIME software. The preamble text should be complete + * lines, including newlines. + * + * @param preamble the preamble text + * @exception MessagingException for failures + * @since JavaMail 1.4 + */ + public synchronized void setPreamble(String preamble) + throws MessagingException { + this.preamble = preamble; + } + + /** + * Update headers. The default implementation here just + * calls the updateHeaders method on each of its + * children BodyParts.

+ * + * Note that the boundary parameter is already set up when + * a new and empty MimeMultipart object is created.

+ * + * This method is called when the saveChanges + * method is invoked on the Message object containing this + * Multipart. This is typically done as part of the Message + * send process, however note that a client is free to call + * it any number of times. So if the header updating process is + * expensive for a specific MimeMultipart subclass, then it + * might itself want to track whether its internal state actually + * did change, and do the header updating only if necessary. + * + * @exception MessagingException for failures + */ + protected synchronized void updateHeaders() throws MessagingException { + parse(); + for (int i = 0; i < parts.size(); i++) + ((MimeBodyPart)parts.elementAt(i)).updateHeaders(); + } + + /** + * Iterates through all the parts and outputs each MIME part + * separated by a boundary. + */ + @Override + public synchronized void writeTo(OutputStream os) + throws IOException, MessagingException { + parse(); + + String boundary = "--" + + (new ContentType(contentType)).getParameter("boundary"); + LineOutputStream los = new LineOutputStream(os); + + // if there's a preamble, write it out + if (preamble != null) { + byte[] pb = ASCIIUtility.getBytes(preamble); + los.write(pb); + // make sure it ends with a newline + if (pb.length > 0 && + !(pb[pb.length-1] == '\r' || pb[pb.length-1] == '\n')) { + los.writeln(); + } + // XXX - could force a blank line before start boundary + } + + if (parts.size() == 0) { + if (allowEmpty) { + // write out a single empty body part + los.writeln(boundary); // put out boundary + los.writeln(); // put out empty line + } else { + throw new MessagingException("Empty multipart: " + contentType); + } + } else { + for (int i = 0; i < parts.size(); i++) { + los.writeln(boundary); // put out boundary + ((MimeBodyPart)parts.elementAt(i)).writeTo(os); + los.writeln(); // put out empty line + } + } + + // put out last boundary + los.writeln(boundary + "--"); + } + + /** + * Parse the InputStream from our DataSource, constructing the + * appropriate MimeBodyParts. The parsed flag is + * set to true, and if true on entry nothing is done. This + * method is called by all other methods that need data for + * the body parts, to make sure the data has been parsed. + * The {@link #initializeProperties} method is called before + * parsing the data. + * + * @exception ParseException for failures parsing the message + * @exception MessagingException for other failures + * @since JavaMail 1.2 + */ + protected synchronized void parse() throws MessagingException { + if (parsed) + return; + + initializeProperties(); + + InputStream in = null; + SharedInputStream sin = null; + long start = 0, end = 0; + + try { + in = ds.getInputStream(); + if (!(in instanceof ByteArrayInputStream) && + !(in instanceof BufferedInputStream) && + !(in instanceof SharedInputStream)) + in = new BufferedInputStream(in); + } catch (Exception ex) { + throw new MessagingException("No inputstream from datasource", ex); + } + if (in instanceof SharedInputStream) + sin = (SharedInputStream)in; + + ContentType cType = new ContentType(contentType); + String boundary = null; + if (!ignoreExistingBoundaryParameter) { + String bp = cType.getParameter("boundary"); + if (bp != null) + boundary = "--" + bp; + } + if (boundary == null && !ignoreMissingBoundaryParameter && + !ignoreExistingBoundaryParameter) + throw new ParseException("Missing boundary parameter"); + + try { + // Skip and save the preamble + LineInputStream lin = new LineInputStream(in); + StringBuilder preamblesb = null; + String line; + while ((line = lin.readLine()) != null) { + /* + * Strip trailing whitespace. Can't use trim method + * because it's too aggressive. Some bogus MIME + * messages will include control characters in the + * boundary string. + */ + int i; + for (i = line.length() - 1; i >= 0; i--) { + char c = line.charAt(i); + if (!(c == ' ' || c == '\t')) + break; + } + line = line.substring(0, i + 1); + if (boundary != null) { + if (line.equals(boundary)) + break; + if (line.length() == boundary.length() + 2 && + line.startsWith(boundary) && line.endsWith("--")) { + line = null; // signal end of multipart + break; + } + } else { + /* + * Boundary hasn't been defined, does this line + * look like a boundary? If so, assume it is + * the boundary and save it. + */ + if (line.length() > 2 && line.startsWith("--")) { + if (line.length() > 4 && allDashes(line)) { + /* + * The first boundary-like line we find is + * probably *not* the end-of-multipart boundary + * line. More likely it's a line full of dashes + * in the preamble text. Just keep reading. + */ + } else { + boundary = line; + break; + } + } + } + + // save the preamble after skipping blank lines + if (line.length() > 0) { + // accumulate the preamble + if (preamblesb == null) + preamblesb = new StringBuilder(line.length() + 2); + preamblesb.append(line).append(System.lineSeparator()); + } + } + + if (preamblesb != null) + preamble = preamblesb.toString(); + + if (line == null) { + if (allowEmpty) + return; + else + throw new ParseException("Missing start boundary"); + } + + // save individual boundary bytes for comparison later + byte[] bndbytes = ASCIIUtility.getBytes(boundary); + int bl = bndbytes.length; + + /* + * Compile Boyer-Moore parsing tables. + */ + + // initialize Bad Character Shift table + int[] bcs = new int[256]; + for (int i = 0; i < bl; i++) + bcs[bndbytes[i] & 0xff] = i + 1; + + // initialize Good Suffix Shift table + int[] gss = new int[bl]; + NEXT: + for (int i = bl; i > 0; i--) { + int j; // the beginning index of the suffix being considered + for (j = bl - 1; j >= i; j--) { + // Testing for good suffix + if (bndbytes[j] == bndbytes[j - i]) { + // bndbytes[j..len] is a good suffix + gss[j - 1] = i; + } else { + // No match. The array has already been + // filled up with correct values before. + continue NEXT; + } + } + while (j > 0) + gss[--j] = i; + } + gss[bl - 1] = 1; + + /* + * Read and process body parts until we see the + * terminating boundary line (or EOF). + */ + boolean done = false; + getparts: + while (!done) { + InternetHeaders headers = null; + if (sin != null) { + start = sin.getPosition(); + // skip headers + while ((line = lin.readLine()) != null && line.length() > 0) + ; + if (line == null) { + if (!ignoreMissingEndBoundary) + throw new ParseException( + "missing multipart end boundary"); + // assume there's just a missing end boundary + complete = false; + break getparts; + } + } else { + // collect the headers for this body part + headers = createInternetHeaders(in); + } + + if (!in.markSupported()) + throw new MessagingException("Stream doesn't support mark"); + + ByteArrayOutputStream buf = null; + // if we don't have a shared input stream, we copy the data + if (sin == null) + buf = new ByteArrayOutputStream(); + else + end = sin.getPosition(); + int b; + + /* + * These buffers contain the bytes we're checking + * for a match. inbuf is the current buffer and + * previnbuf is the previous buffer. We need the + * previous buffer to check that we're preceeded + * by an EOL. + */ + // XXX - a smarter algorithm would use a sliding window + // over a larger buffer + byte[] inbuf = new byte[bl]; + byte[] previnbuf = new byte[bl]; + int inSize = 0; // number of valid bytes in inbuf + int prevSize = 0; // number of valid bytes in previnbuf + int eolLen; + boolean first = true; + + /* + * Read and save the content bytes in buf. + */ + for (;;) { + in.mark(bl + 4 + 1000); // bnd + "--\r\n" + lots of LWSP + eolLen = 0; + inSize = readFully(in, inbuf, 0, bl); + if (inSize < bl) { + // hit EOF + if (!ignoreMissingEndBoundary) + throw new ParseException( + "missing multipart end boundary"); + if (sin != null) + end = sin.getPosition(); + complete = false; + done = true; + break; + } + // check whether inbuf contains a boundary string + int i; + for (i = bl - 1; i >= 0; i--) { + if (inbuf[i] != bndbytes[i]) + break; + } + if (i < 0) { // matched all bytes + eolLen = 0; + if (!first) { + // working backwards, find out if we were preceeded + // by an EOL, and if so find its length + b = previnbuf[prevSize - 1]; + if (b == '\r' || b == '\n') { + eolLen = 1; + if (b == '\n' && prevSize >= 2) { + b = previnbuf[prevSize - 2]; + if (b == '\r') + eolLen = 2; + } + } + } + if (first || eolLen > 0) { // yes, preceed by EOL + if (sin != null) { + // update "end", in case this really is + // a valid boundary + end = sin.getPosition() - bl - eolLen; + } + // matched the boundary, check for last boundary + int b2 = in.read(); + if (b2 == '-') { + if (in.read() == '-') { + complete = true; + done = true; + break; // ignore trailing text + } + } + // skip linear whitespace + while (b2 == ' ' || b2 == '\t') + b2 = in.read(); + // check for end of line + if (b2 == '\n') + break; // got it! break out of the loop + if (b2 == '\r') { + in.mark(1); + if (in.read() != '\n') + in.reset(); + break; // got it! break out of the loop + } + } + i = 0; + } + + /* + * Get here if boundary didn't match, + * wasn't preceeded by EOL, or wasn't + * followed by whitespace or EOL. + */ + + // compute how many bytes we can skip + int skip = Math.max(i + 1 - bcs[inbuf[i] & 0x7f], gss[i]); + // want to keep at least two characters + if (skip < 2) { + // only skipping one byte, save one byte + // from previous buffer as well + // first, write out bytes we're done with + if (sin == null && prevSize > 1) + buf.write(previnbuf, 0, prevSize - 1); + in.reset(); + skipFully(in, 1); + if (prevSize >= 1) { // is there a byte to save? + // yes, save one from previous and one from current + previnbuf[0] = previnbuf[prevSize - 1]; + previnbuf[1] = inbuf[0]; + prevSize = 2; + } else { + // no previous bytes to save, can only save current + previnbuf[0] = inbuf[0]; + prevSize = 1; + } + } else { + // first, write out data from previous buffer before + // we dump it + if (prevSize > 0 && sin == null) + buf.write(previnbuf, 0, prevSize); + // all the bytes we're skipping are saved in previnbuf + prevSize = skip; + in.reset(); + skipFully(in, prevSize); + // swap buffers + byte[] tmp = inbuf; + inbuf = previnbuf; + previnbuf = tmp; + } + first = false; + } + + /* + * Create a MimeBody element to represent this body part. + */ + MimeBodyPart part; + if (sin != null) { + part = createMimeBodyPartIs(sin.newStream(start, end)); + } else { + // write out data from previous buffer, not including EOL + if (prevSize - eolLen > 0) + buf.write(previnbuf, 0, prevSize - eolLen); + // if we didn't find a trailing boundary, + // the current buffer has data we need too + if (!complete && inSize > 0) + buf.write(inbuf, 0, inSize); + part = createMimeBodyPart(headers, buf.toByteArray()); + } + super.addBodyPart(part); + } + } catch (IOException ioex) { + throw new MessagingException("IO Error", ioex); + } finally { + try { + in.close(); + } catch (IOException cex) { + // ignore + } + } + + parsed = true; + } + + /** + * Is the string all dashes ('-')? + */ + private static boolean allDashes(String s) { + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) != '-') + return false; + } + return true; + } + + /** + * Read data from the input stream to fill the buffer starting + * at the specified offset with the specified number of bytes. + * If len is zero, return zero. If at EOF, return -1. Otherwise, + * return the number of bytes read. Call the read method on the + * input stream as many times as necessary to read len bytes. + * + * @param in InputStream to read from + * @param buf buffer to read into + * @param off offset in the buffer for first byte + * @param len number of bytes to read + * @return -1 on EOF, otherwise number of bytes read + * @exception IOException on I/O errors + */ + private static int readFully(InputStream in, byte[] buf, int off, int len) + throws IOException { + if (len == 0) + return 0; + int total = 0; + while (len > 0) { + int bsize = in.read(buf, off, len); + if (bsize <= 0) // should never be zero + break; + off += bsize; + total += bsize; + len -= bsize; + } + return total > 0 ? total : -1; + } + + /** + * Skip the specified number of bytes, repeatedly calling + * the skip method as necessary. + */ + private void skipFully(InputStream in, long offset) throws IOException { + while (offset > 0) { + long cur = in.skip(offset); + if (cur <= 0) + throw new EOFException("can't skip"); + offset -= cur; + } + } + + /** + * Create and return an InternetHeaders object that loads the + * headers from the given InputStream. Subclasses can override + * this method to return a subclass of InternetHeaders, if + * necessary. This implementation simply constructs and returns + * an InternetHeaders object. + * + * @param is the InputStream to read the headers from + * @return an InternetHeaders object + * @exception MessagingException for failures + * @since JavaMail 1.2 + */ + protected InternetHeaders createInternetHeaders(InputStream is) + throws MessagingException { + return new InternetHeaders(is); + } + + /** + * Create and return a MimeBodyPart object to represent a + * body part parsed from the InputStream. Subclasses can override + * this method to return a subclass of MimeBodyPart, if + * necessary. This implementation simply constructs and returns + * a MimeBodyPart object. + * + * @param headers the headers for the body part + * @param content the content of the body part + * @return a MimeBodyPart + * @exception MessagingException for failures + * @since JavaMail 1.2 + */ + protected MimeBodyPart createMimeBodyPart(InternetHeaders headers, + byte[] content) throws MessagingException { + return new MimeBodyPart(headers, content); + } + + /** + * Create and return a MimeBodyPart object to represent a + * body part parsed from the InputStream. Subclasses can override + * this method to return a subclass of MimeBodyPart, if + * necessary. This implementation simply constructs and returns + * a MimeBodyPart object. + * + * @param is InputStream containing the body part + * @return a MimeBodyPart + * @exception MessagingException for failures + * @since JavaMail 1.2 + */ + protected MimeBodyPart createMimeBodyPart(InputStream is) + throws MessagingException { + return new MimeBodyPart(is); + } + + private MimeBodyPart createMimeBodyPartIs(InputStream is) + throws MessagingException { + try { + return createMimeBodyPart(is); + } finally { + try { + is.close(); + } catch (IOException ex) { + // ignore it + } + } + } +} diff --git a/app/src/main/java/javax/mail/internet/MimePart.java b/app/src/main/java/javax/mail/internet/MimePart.java new file mode 100644 index 0000000000..b43fc4ae3d --- /dev/null +++ b/app/src/main/java/javax/mail/internet/MimePart.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import javax.mail.*; +import java.io.*; +import java.util.Enumeration; + +/** + * The MimePart interface models an Entity as defined + * by MIME (RFC2045, Section 2.4).

+ * + * MimePart extends the Part interface to add additional RFC822 and MIME + * specific semantics and attributes. It provides the base interface for + * the MimeMessage and MimeBodyPart classes + * + *


A note on RFC822 and MIME headers

+ * + * RFC822 and MIME header fields must contain only + * US-ASCII characters. If a header contains non US-ASCII characters, + * it must be encoded as per the rules in RFC 2047. The MimeUtility + * class provided in this package can be used to to achieve this. + * Callers of the setHeader, addHeader, and + * addHeaderLine methods are responsible for enforcing + * the MIME requirements for the specified headers. In addition, these + * header fields must be folded (wrapped) before being sent if they + * exceed the line length limitation for the transport (1000 bytes for + * SMTP). Received headers may have been folded. The application is + * responsible for folding and unfolding headers as appropriate.

+ * + * @see MimeUtility + * @see javax.mail.Part + * @author John Mani + */ + +public interface MimePart extends Part { + + /** + * Get the values of all header fields available for this header, + * returned as a single String, with the values separated by the + * delimiter. If the delimiter is null, only the + * first value is returned. + * + * @param name the name of this header + * @param delimiter delimiter between fields in returned string + * @return the value fields for all headers with + * this name + * @exception MessagingException for failures + */ + public String getHeader(String name, String delimiter) + throws MessagingException; + + /** + * Add a raw RFC822 header-line. + * + * @param line the line to add + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this Part is + * obtained from a READ_ONLY folder + * @exception MessagingException for other failures + */ + public void addHeaderLine(String line) throws MessagingException; + + /** + * Get all header lines as an Enumeration of Strings. A Header + * line is a raw RFC822 header-line, containing both the "name" + * and "value" field. + * + * @return an Enumeration of Strings + * @exception MessagingException for failures + */ + public Enumeration getAllHeaderLines() throws MessagingException; + + /** + * Get matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC822 header-line, containing both + * the "name" and "value" field. + * + * @param names the headers to return + * @return an Enumeration of Strings + * @exception MessagingException for failures + */ + public Enumeration getMatchingHeaderLines(String[] names) + throws MessagingException; + + /** + * Get non-matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC822 header-line, containing both + * the "name" and "value" field. + * + * @param names the headers to not return + * @return an Enumeration of Strings + * @exception MessagingException for failures + */ + public Enumeration getNonMatchingHeaderLines(String[] names) + throws MessagingException; + + /** + * Get the transfer encoding of this part. + * + * @return content-transfer-encoding + * @exception MessagingException for failures + */ + public String getEncoding() throws MessagingException; + + /** + * Get the Content-ID of this part. Returns null if none present. + * + * @return content-ID + * @exception MessagingException for failures + */ + public String getContentID() throws MessagingException; + + /** + * Get the Content-MD5 digest of this part. Returns null if + * none present. + * + * @return content-MD5 + * @exception MessagingException for failures + */ + public String getContentMD5() throws MessagingException; + + /** + * Set the Content-MD5 of this part. + * + * @param md5 the MD5 value + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this Part is + * obtained from a READ_ONLY folder + */ + public void setContentMD5(String md5) throws MessagingException; + + /** + * Get the language tags specified in the Content-Language header + * of this MimePart. The Content-Language header is defined by + * RFC 1766. Returns null if this header is not + * available. + * + * @return array of content language strings + * @exception MessagingException for failures + */ + public String[] getContentLanguage() throws MessagingException; + + /** + * Set the Content-Language header of this MimePart. The + * Content-Language header is defined by RFC1766. + * + * @param languages array of language tags + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this Part is + * obtained from a READ_ONLY folder + */ + public void setContentLanguage(String[] languages) + throws MessagingException; + + /** + * Convenience method that sets the given String as this + * part's content, with a MIME type of "text/plain". If the + * string contains non US-ASCII characters. it will be encoded + * using the platform's default charset. The charset is also + * used to set the "charset" parameter.

+ * + * Note that there may be a performance penalty if + * text is large, since this method may have + * to scan all the characters to determine what charset to + * use.

+ * + * If the charset is already known, use the + * setText method that takes the charset parameter. + * + * @param text the text content to set + * @exception MessagingException if an error occurs + * @see #setText(String text, String charset) + */ + @Override + public void setText(String text) throws MessagingException; + + /** + * Convenience method that sets the given String as this part's + * content, with a MIME type of "text/plain" and the specified + * charset. The given Unicode string will be charset-encoded + * using the specified charset. The charset is also used to set + * "charset" parameter. + * + * @param text the text content to set + * @param charset the charset to use for the text + * @exception MessagingException if an error occurs + */ + public void setText(String text, String charset) + throws MessagingException; + + /** + * Convenience method that sets the given String as this part's + * content, with a primary MIME type of "text" and the specified + * MIME subtype. The given Unicode string will be charset-encoded + * using the specified charset. The charset is also used to set + * the "charset" parameter. + * + * @param text the text content to set + * @param charset the charset to use for the text + * @param subtype the MIME subtype to use (e.g., "html") + * @exception MessagingException if an error occurs + * @since JavaMail 1.4 + */ + public void setText(String text, String charset, String subtype) + throws MessagingException; +} diff --git a/app/src/main/java/javax/mail/internet/MimePartDataSource.java b/app/src/main/java/javax/mail/internet/MimePartDataSource.java new file mode 100644 index 0000000000..0e481aef89 --- /dev/null +++ b/app/src/main/java/javax/mail/internet/MimePartDataSource.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import javax.mail.*; +import javax.activation.*; +import java.io.*; +import java.net.UnknownServiceException; +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.FolderClosedIOException; + +/** + * A utility class that implements a DataSource out of + * a MimePart. This class is primarily meant for service providers. + * + * @see javax.mail.internet.MimePart + * @see javax.activation.DataSource + * @author John Mani + */ + +public class MimePartDataSource implements DataSource, MessageAware { + /** + * The MimePart that provides the data for this DataSource. + * + * @since JavaMail 1.4 + */ + protected MimePart part; + + private MessageContext context; + + /** + * Constructor, that constructs a DataSource from a MimePart. + * + * @param part the MimePart + */ + public MimePartDataSource(MimePart part) { + this.part = part; + } + + /** + * Returns an input stream from this MimePart.

+ * + * This method applies the appropriate transfer-decoding, based + * on the Content-Transfer-Encoding attribute of this MimePart. + * Thus the returned input stream is a decoded stream of bytes.

+ * + * This implementation obtains the raw content from the Part + * using the getContentStream() method and decodes + * it using the MimeUtility.decode() method. + * + * @see javax.mail.internet.MimeMessage#getContentStream + * @see javax.mail.internet.MimeBodyPart#getContentStream + * @see javax.mail.internet.MimeUtility#decode + * @return decoded input stream + */ + @Override + public InputStream getInputStream() throws IOException { + InputStream is; + + try { + if (part instanceof MimeBodyPart) + is = ((MimeBodyPart)part).getContentStream(); + else if (part instanceof MimeMessage) + is = ((MimeMessage)part).getContentStream(); + else + throw new MessagingException("Unknown part"); + + String encoding = + MimeBodyPart.restrictEncoding(part, part.getEncoding()); + if (encoding != null) + return MimeUtility.decode(is, encoding); + else + return is; + } catch (FolderClosedException fex) { + throw new FolderClosedIOException(fex.getFolder(), + fex.getMessage()); + } catch (MessagingException mex) { + IOException ioex = new IOException(mex.getMessage()); + ioex.initCause(mex); + throw ioex; + } + } + + /** + * DataSource method to return an output stream.

+ * + * This implementation throws the UnknownServiceException. + */ + @Override + public OutputStream getOutputStream() throws IOException { + throw new UnknownServiceException("Writing not supported"); + } + + /** + * Returns the content-type of this DataSource.

+ * + * This implementation just invokes the getContentType + * method on the MimePart. + */ + @Override + public String getContentType() { + try { + return part.getContentType(); + } catch (MessagingException mex) { + // would like to be able to reflect the exception to the + // application, but since we can't do that we return a + // generic "unknown" value here and hope for another + // exception later. + return "application/octet-stream"; + } + } + + /** + * DataSource method to return a name.

+ * + * This implementation just returns an empty string. + */ + @Override + public String getName() { + try { + if (part instanceof MimeBodyPart) + return ((MimeBodyPart)part).getFileName(); + } catch (MessagingException mex) { + // ignore it + } + return ""; + } + + /** + * Return the MessageContext for the current part. + * @since JavaMail 1.1 + */ + @Override + public synchronized MessageContext getMessageContext() { + if (context == null) + context = new MessageContext(part); + return context; + } +} diff --git a/app/src/main/java/javax/mail/internet/MimeUtility.java b/app/src/main/java/javax/mail/internet/MimeUtility.java new file mode 100644 index 0000000000..f4bae05a6c --- /dev/null +++ b/app/src/main/java/javax/mail/internet/MimeUtility.java @@ -0,0 +1,1708 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import javax.mail.MessagingException; +import javax.mail.EncodingAware; +import javax.activation.*; +import java.util.*; +import java.io.*; +import java.nio.charset.Charset; +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.util.BASE64DecoderStream; +import com.sun.mail.util.BASE64EncoderStream; +import com.sun.mail.util.BEncoderStream; +import com.sun.mail.util.LineInputStream; +import com.sun.mail.util.LineOutputStream; +import com.sun.mail.util.LogOutputStream; +import com.sun.mail.util.QDecoderStream; +import com.sun.mail.util.QEncoderStream; +import com.sun.mail.util.QPDecoderStream; +import com.sun.mail.util.QPEncoderStream; +import com.sun.mail.util.UUDecoderStream; +import com.sun.mail.util.UUEncoderStream; + +/** + * This is a utility class that provides various MIME related + * functionality.

+ * + * There are a set of methods to encode and decode MIME headers as + * per RFC 2047. Note that, in general, these methods are + * not needed when using methods such as + * setSubject and setRecipients; Jakarta Mail + * will automatically encode and decode data when using these "higher + * level" methods. The methods below are only needed when maniuplating + * raw MIME headers using setHeader and getHeader + * methods. A brief description on handling such headers is given below:

+ * + * RFC 822 mail headers must contain only US-ASCII + * characters. Headers that contain non US-ASCII characters must be + * encoded so that they contain only US-ASCII characters. Basically, + * this process involves using either BASE64 or QP to encode certain + * characters. RFC 2047 describes this in detail.

+ * + * In Java, Strings contain (16 bit) Unicode characters. ASCII is a + * subset of Unicode (and occupies the range 0 - 127). A String + * that contains only ASCII characters is already mail-safe. If the + * String contains non US-ASCII characters, it must be encoded. An + * additional complexity in this step is that since Unicode is not + * yet a widely used charset, one might want to first charset-encode + * the String into another charset and then do the transfer-encoding. + *

+ * Note that to get the actual bytes of a mail-safe String (say, + * for sending over SMTP), one must do + *

+ *
+ *	byte[] bytes = string.getBytes("iso-8859-1");	
+ *
+ * 

+ * + * The setHeader and addHeader methods + * on MimeMessage and MimeBodyPart assume that the given header values + * are Unicode strings that contain only US-ASCII characters. Hence + * the callers of those methods must insure that the values they pass + * do not contain non US-ASCII characters. The methods in this class + * help do this.

+ * + * The getHeader family of methods on MimeMessage and + * MimeBodyPart return the raw header value. These might be encoded + * as per RFC 2047, and if so, must be decoded into Unicode Strings. + * The methods in this class help to do this.

+ * + * Several System properties control strict conformance to the MIME + * spec. Note that these are not session properties but must be set + * globally as System properties.

+ * + * The mail.mime.decodetext.strict property controls + * decoding of MIME encoded words. The MIME spec requires that encoded + * words start at the beginning of a whitespace separated word. Some + * mailers incorrectly include encoded words in the middle of a word. + * If the mail.mime.decodetext.strict System property is + * set to "false", an attempt will be made to decode these + * illegal encoded words. The default is true.

+ * + * The mail.mime.encodeeol.strict property controls the + * choice of Content-Transfer-Encoding for MIME parts that are not of + * type "text". Often such parts will contain textual data for which + * an encoding that allows normal end of line conventions is appropriate. + * In rare cases, such a part will appear to contain entirely textual + * data, but will require an encoding that preserves CR and LF characters + * without change. If the mail.mime.encodeeol.strict + * System property is set to "true", such an encoding will + * be used when necessary. The default is false.

+ * + * In addition, the mail.mime.charset System property can + * be used to specify the default MIME charset to use for encoded words + * and text parts that don't otherwise specify a charset. Normally, the + * default MIME charset is derived from the default Java charset, as + * specified in the file.encoding System property. Most + * applications will have no need to explicitly set the default MIME + * charset. In cases where the default MIME charset to be used for + * mail messages is different than the charset used for files stored on + * the system, this property should be set.

+ * + * The current implementation also supports the following System property. + *

+ * The mail.mime.ignoreunknownencoding property controls + * whether unknown values in the Content-Transfer-Encoding + * header, as passed to the decode method, cause an exception. + * If set to "true", unknown values are ignored and 8bit + * encoding is assumed. Otherwise, unknown values cause a MessagingException + * to be thrown. + * + * @author John Mani + * @author Bill Shannon + */ + +public class MimeUtility { + + // This class cannot be instantiated + private MimeUtility() { } + + public static final int ALL = -1; + + // cached map of whether a charset is compatible with ASCII + // Map + private static final Map nonAsciiCharsetMap + = new HashMap<>(); + + private static final boolean decodeStrict = + PropUtil.getBooleanSystemProperty("mail.mime.decodetext.strict", true); + private static final boolean encodeEolStrict = + PropUtil.getBooleanSystemProperty("mail.mime.encodeeol.strict", false); + private static final boolean ignoreUnknownEncoding = + PropUtil.getBooleanSystemProperty( + "mail.mime.ignoreunknownencoding", false); + private static final boolean allowUtf8 = + PropUtil.getBooleanSystemProperty("mail.mime.allowutf8", false); + /* + * The following two properties allow disabling the fold() + * and unfold() methods and reverting to the previous behavior. + * They should never need to be changed and are here only because + * of my paranoid concern with compatibility. + */ + private static final boolean foldEncodedWords = + PropUtil.getBooleanSystemProperty("mail.mime.foldencodedwords", false); + private static final boolean foldText = + PropUtil.getBooleanSystemProperty("mail.mime.foldtext", true); + + + /** + * Get the Content-Transfer-Encoding that should be applied + * to the input stream of this DataSource, to make it mail-safe.

+ * + * The algorithm used here is:
+ *

    + *
  • + * If the DataSource implements {@link EncodingAware}, ask it + * what encoding to use. If it returns non-null, return that value. + *
  • + * If the primary type of this datasource is "text" and if all + * the bytes in its input stream are US-ASCII, then the encoding + * is "7bit". If more than half of the bytes are non-US-ASCII, then + * the encoding is "base64". If less than half of the bytes are + * non-US-ASCII, then the encoding is "quoted-printable". + *
  • + * If the primary type of this datasource is not "text", then if + * all the bytes of its input stream are US-ASCII, the encoding + * is "7bit". If there is even one non-US-ASCII character, the + * encoding is "base64". + *
+ * + * @param ds the DataSource + * @return the encoding. This is either "7bit", + * "quoted-printable" or "base64" + */ + public static String getEncoding(DataSource ds) { + ContentType cType = null; + InputStream is = null; + String encoding = null; + + if (ds instanceof EncodingAware) { + encoding = ((EncodingAware)ds).getEncoding(); + if (encoding != null) + return encoding; + } + try { + cType = new ContentType(ds.getContentType()); + is = ds.getInputStream(); + + boolean isText = cType.match("text/*"); + // if not text, stop processing when we see non-ASCII + int i = checkAscii(is, ALL, !isText); + switch (i) { + case ALL_ASCII: + encoding = "7bit"; // all ASCII + break; + case MOSTLY_ASCII: + if (isText && nonAsciiCharset(cType)) + encoding = "base64"; // charset isn't compatible with ASCII + else + encoding = "quoted-printable"; // mostly ASCII + break; + default: + encoding = "base64"; // mostly binary + break; + } + + } catch (Exception ex) { + return "base64"; // what else ?! + } finally { + // Close the input stream + try { + if (is != null) + is.close(); + } catch (IOException ioex) { } + } + + return encoding; + } + + /** + * Determine whether the charset in the Content-Type is compatible + * with ASCII or not. A charset is compatible with ASCII if the + * encoded byte stream representing the Unicode string "\r\n" is + * the ASCII characters CR and LF. For example, the utf-16be + * charset is not compatible with ASCII. + * + * For performance, we keep a static map that caches the results. + */ + private static boolean nonAsciiCharset(ContentType ct) { + String charset = ct.getParameter("charset"); + if (charset == null) + return false; + charset = charset.toLowerCase(Locale.ENGLISH); + Boolean bool; + synchronized (nonAsciiCharsetMap) { + bool = nonAsciiCharsetMap.get(charset); + } + if (bool == null) { + try { + byte[] b = "\r\n".getBytes(charset); + bool = Boolean.valueOf( + b.length != 2 || b[0] != 015 || b[1] != 012); + } catch (UnsupportedEncodingException uex) { + bool = Boolean.FALSE; // a guess + } catch (RuntimeException ex) { + bool = Boolean.TRUE; // one of the weird ones? + } + synchronized (nonAsciiCharsetMap) { + nonAsciiCharsetMap.put(charset, bool); + } + } + return bool.booleanValue(); + } + + /** + * Same as getEncoding(DataSource) except that instead + * of reading the data from an InputStream it uses the + * writeTo method to examine the data. This is more + * efficient in the common case of a DataHandler + * created with an object and a MIME type (for example, a + * "text/plain" String) because all the I/O is done in this + * thread. In the case requiring an InputStream the + * DataHandler uses a thread, a pair of pipe streams, + * and the writeTo method to produce the data.

+ * + * @param dh the DataHandler + * @return the Content-Transfer-Encoding + * @since JavaMail 1.2 + */ + public static String getEncoding(DataHandler dh) { + ContentType cType = null; + String encoding = null; + + /* + * Try to pick the most efficient means of determining the + * encoding. If this DataHandler was created using a DataSource, + * the getEncoding(DataSource) method is typically faster. If + * the DataHandler was created with an object, this method is + * much faster. To distinguish the two cases, we use a heuristic. + * A DataHandler created with an object will always have a null name. + * A DataHandler created with a DataSource will usually have a + * non-null name. + * + * XXX - This is actually quite a disgusting hack, but it makes + * a common case run over twice as fast. + */ + if (dh.getName() != null) + return getEncoding(dh.getDataSource()); + + try { + cType = new ContentType(dh.getContentType()); + } catch (Exception ex) { + return "base64"; // what else ?! + } + + if (cType.match("text/*")) { + // Check all of the available bytes + AsciiOutputStream aos = new AsciiOutputStream(false, false); + try { + dh.writeTo(aos); + } catch (IOException ex) { + // ignore it, can't happen + } + switch (aos.getAscii()) { + case ALL_ASCII: + encoding = "7bit"; // all ascii + break; + case MOSTLY_ASCII: + encoding = "quoted-printable"; // mostly ascii + break; + default: + encoding = "base64"; // mostly binary + break; + } + } else { // not "text" + // Check all of available bytes, break out if we find + // at least one non-US-ASCII character + AsciiOutputStream aos = + new AsciiOutputStream(true, encodeEolStrict); + try { + dh.writeTo(aos); + } catch (IOException ex) { } // ignore it + if (aos.getAscii() == ALL_ASCII) // all ascii + encoding = "7bit"; + else // found atleast one non-ascii character, use b64 + encoding = "base64"; + } + + return encoding; + } + + /** + * Decode the given input stream. The Input stream returned is + * the decoded input stream. All the encodings defined in RFC 2045 + * are supported here. They include "base64", "quoted-printable", + * "7bit", "8bit", and "binary". In addition, "uuencode" is also + * supported.

+ * + * In the current implementation, if the + * mail.mime.ignoreunknownencoding system property is set to + * "true", unknown encoding values are ignored and the + * original InputStream is returned. + * + * @param is input stream + * @param encoding the encoding of the stream. + * @return decoded input stream. + * @exception MessagingException if the encoding is unknown + */ + public static InputStream decode(InputStream is, String encoding) + throws MessagingException { + if (encoding.equalsIgnoreCase("base64")) + return new BASE64DecoderStream(is); + else if (encoding.equalsIgnoreCase("quoted-printable")) + return new QPDecoderStream(is); + else if (encoding.equalsIgnoreCase("uuencode") || + encoding.equalsIgnoreCase("x-uuencode") || + encoding.equalsIgnoreCase("x-uue")) + return new UUDecoderStream(is); + else if (encoding.equalsIgnoreCase("binary") || + encoding.equalsIgnoreCase("7bit") || + encoding.equalsIgnoreCase("8bit")) + return is; + else { + if (!ignoreUnknownEncoding) + throw new MessagingException("Unknown encoding: " + encoding); + return is; + } + } + + /** + * Wrap an encoder around the given output stream. + * All the encodings defined in RFC 2045 are supported here. + * They include "base64", "quoted-printable", "7bit", "8bit" and + * "binary". In addition, "uuencode" is also supported. + * + * @param os output stream + * @param encoding the encoding of the stream. + * @return output stream that applies the + * specified encoding. + * @exception MessagingException if the encoding is unknown + */ + public static OutputStream encode(OutputStream os, String encoding) + throws MessagingException { + if (encoding == null) + return os; + else if (encoding.equalsIgnoreCase("base64")) + return new BASE64EncoderStream(os); + else if (encoding.equalsIgnoreCase("quoted-printable")) + return new QPEncoderStream(os); + else if (encoding.equalsIgnoreCase("uuencode") || + encoding.equalsIgnoreCase("x-uuencode") || + encoding.equalsIgnoreCase("x-uue")) + return new UUEncoderStream(os); + else if (encoding.equalsIgnoreCase("binary") || + encoding.equalsIgnoreCase("7bit") || + encoding.equalsIgnoreCase("8bit")) + return os; + else + throw new MessagingException("Unknown encoding: " +encoding); + } + + /** + * Wrap an encoder around the given output stream. + * All the encodings defined in RFC 2045 are supported here. + * They include "base64", "quoted-printable", "7bit", "8bit" and + * "binary". In addition, "uuencode" is also supported. + * The filename parameter is used with the "uuencode" + * encoding and is included in the encoded output. + * + * @param os output stream + * @param encoding the encoding of the stream. + * @param filename name for the file being encoded (only used + * with uuencode) + * @return output stream that applies the + * specified encoding. + * @exception MessagingException for unknown encodings + * @since JavaMail 1.2 + */ + public static OutputStream encode(OutputStream os, String encoding, + String filename) + throws MessagingException { + if (encoding == null) + return os; + else if (encoding.equalsIgnoreCase("base64")) + return new BASE64EncoderStream(os); + else if (encoding.equalsIgnoreCase("quoted-printable")) + return new QPEncoderStream(os); + else if (encoding.equalsIgnoreCase("uuencode") || + encoding.equalsIgnoreCase("x-uuencode") || + encoding.equalsIgnoreCase("x-uue")) + return new UUEncoderStream(os, filename); + else if (encoding.equalsIgnoreCase("binary") || + encoding.equalsIgnoreCase("7bit") || + encoding.equalsIgnoreCase("8bit")) + return os; + else + throw new MessagingException("Unknown encoding: " +encoding); + } + + /** + * Encode a RFC 822 "text" token into mail-safe form as per + * RFC 2047.

+ * + * The given Unicode string is examined for non US-ASCII + * characters. If the string contains only US-ASCII characters, + * it is returned as-is. If the string contains non US-ASCII + * characters, it is first character-encoded using the platform's + * default charset, then transfer-encoded using either the B or + * Q encoding. The resulting bytes are then returned as a Unicode + * string containing only ASCII characters.

+ * + * Note that this method should be used to encode only + * "unstructured" RFC 822 headers.

+ * + * Example of usage: + *

+     *
+     *  MimePart part = ...
+     *  String rawvalue = "FooBar Mailer, Japanese version 1.1"
+     *  try {
+     *    // If we know for sure that rawvalue contains only US-ASCII 
+     *    // characters, we can skip the encoding part
+     *    part.setHeader("X-mailer", MimeUtility.encodeText(rawvalue));
+     *  } catch (UnsupportedEncodingException e) {
+     *    // encoding failure
+     *  } catch (MessagingException me) {
+     *   // setHeader() failure
+     *  }
+     *
+     * 

+ * + * @param text Unicode string + * @return Unicode string containing only US-ASCII characters + * @exception UnsupportedEncodingException if the encoding fails + */ + public static String encodeText(String text) + throws UnsupportedEncodingException { + return encodeText(text, null, null); + } + + /** + * Encode a RFC 822 "text" token into mail-safe form as per + * RFC 2047.

+ * + * The given Unicode string is examined for non US-ASCII + * characters. If the string contains only US-ASCII characters, + * it is returned as-is. If the string contains non US-ASCII + * characters, it is first character-encoded using the specified + * charset, then transfer-encoded using either the B or Q encoding. + * The resulting bytes are then returned as a Unicode string + * containing only ASCII characters.

+ * + * Note that this method should be used to encode only + * "unstructured" RFC 822 headers. + * + * @param text the header value + * @param charset the charset. If this parameter is null, the + * platform's default chatset is used. + * @param encoding the encoding to be used. Currently supported + * values are "B" and "Q". If this parameter is null, then + * the "Q" encoding is used if most of characters to be + * encoded are in the ASCII charset, otherwise "B" encoding + * is used. + * @return Unicode string containing only US-ASCII characters + * @exception UnsupportedEncodingException if the charset + * conversion failed. + */ + public static String encodeText(String text, String charset, + String encoding) + throws UnsupportedEncodingException { + return encodeWord(text, charset, encoding, false); + } + + /** + * Decode "unstructured" headers, that is, headers that are defined + * as '*text' as per RFC 822.

+ * + * The string is decoded using the algorithm specified in + * RFC 2047, Section 6.1. If the charset-conversion fails + * for any sequence, an UnsupportedEncodingException is thrown. + * If the String is not an RFC 2047 style encoded header, it is + * returned as-is

+ * + * Example of usage: + *

+     *
+     *  MimePart part = ...
+     *  String rawvalue = null;
+     *  String  value = null;
+     *  try {
+     *    if ((rawvalue = part.getHeader("X-mailer")[0]) != null)
+     *      value = MimeUtility.decodeText(rawvalue);
+     *  } catch (UnsupportedEncodingException e) {
+     *      // Don't care
+     *      value = rawvalue;
+     *  } catch (MessagingException me) { }
+     *
+     *  return value;
+     *
+     * 

+ * + * @param etext the possibly encoded value + * @return the decoded text + * @exception UnsupportedEncodingException if the charset + * conversion failed. + */ + public static String decodeText(String etext) + throws UnsupportedEncodingException { + /* + * We look for sequences separated by "linear-white-space". + * (as per RFC 2047, Section 6.1) + * RFC 822 defines "linear-white-space" as SPACE | HT | CR | NL. + */ + String lwsp = " \t\n\r"; + StringTokenizer st; + + /* + * First, lets do a quick run thru the string and check + * whether the sequence "=?" exists at all. If none exists, + * we know there are no encoded-words in here and we can just + * return the string as-is, without suffering thru the later + * decoding logic. + * This handles the most common case of unencoded headers + * efficiently. + */ + if (etext.indexOf("=?") == -1) + return etext; + + // Encoded words found. Start decoding ... + + st = new StringTokenizer(etext, lwsp, true); + StringBuilder sb = new StringBuilder(); // decode buffer + StringBuilder wsb = new StringBuilder(); // white space buffer + boolean prevWasEncoded = false; + + while (st.hasMoreTokens()) { + char c; + String s = st.nextToken(); + // If whitespace, append it to the whitespace buffer + if (((c = s.charAt(0)) == ' ') || (c == '\t') || + (c == '\r') || (c == '\n')) + wsb.append(c); + else { + // Check if token is an 'encoded-word' .. + String word; + try { + word = decodeWord(s); + // Yes, this IS an 'encoded-word'. + if (!prevWasEncoded && wsb.length() > 0) { + // if the previous word was also encoded, we + // should ignore the collected whitespace. Else + // we include the whitespace as well. + sb.append(wsb); + } + prevWasEncoded = true; + } catch (ParseException pex) { + // This is NOT an 'encoded-word'. + word = s; + // possibly decode inner encoded words + if (!decodeStrict) { + String dword = decodeInnerWords(word); + if (dword != word) { + // if a different String object was returned, + // decoding was done. + if (prevWasEncoded && word.startsWith("=?")) { + // encoded followed by encoded, + // throw away whitespace between + } else { + // include collected whitespace .. + if (wsb.length() > 0) + sb.append(wsb); + } + // did original end with encoded? + prevWasEncoded = word.endsWith("?="); + word = dword; + } else { + // include collected whitespace .. + if (wsb.length() > 0) + sb.append(wsb); + prevWasEncoded = false; + } + } else { + // include collected whitespace .. + if (wsb.length() > 0) + sb.append(wsb); + prevWasEncoded = false; + } + } + sb.append(word); // append the actual word + wsb.setLength(0); // reset wsb for reuse + } + } + sb.append(wsb); // append trailing whitespace + return sb.toString(); + } + + /** + * Encode a RFC 822 "word" token into mail-safe form as per + * RFC 2047.

+ * + * The given Unicode string is examined for non US-ASCII + * characters. If the string contains only US-ASCII characters, + * it is returned as-is. If the string contains non US-ASCII + * characters, it is first character-encoded using the platform's + * default charset, then transfer-encoded using either the B or + * Q encoding. The resulting bytes are then returned as a Unicode + * string containing only ASCII characters.

+ * + * This method is meant to be used when creating RFC 822 "phrases". + * The InternetAddress class, for example, uses this to encode + * it's 'phrase' component. + * + * @param word Unicode string + * @return Array of Unicode strings containing only US-ASCII + * characters. + * @exception UnsupportedEncodingException if the encoding fails + */ + public static String encodeWord(String word) + throws UnsupportedEncodingException { + return encodeWord(word, null, null); + } + + /** + * Encode a RFC 822 "word" token into mail-safe form as per + * RFC 2047.

+ * + * The given Unicode string is examined for non US-ASCII + * characters. If the string contains only US-ASCII characters, + * it is returned as-is. If the string contains non US-ASCII + * characters, it is first character-encoded using the specified + * charset, then transfer-encoded using either the B or Q encoding. + * The resulting bytes are then returned as a Unicode string + * containing only ASCII characters.

+ * + * @param word Unicode string + * @param charset the MIME charset + * @param encoding the encoding to be used. Currently supported + * values are "B" and "Q". If this parameter is null, then + * the "Q" encoding is used if most of characters to be + * encoded are in the ASCII charset, otherwise "B" encoding + * is used. + * @return Unicode string containing only US-ASCII characters + * @exception UnsupportedEncodingException if the encoding fails + */ + public static String encodeWord(String word, String charset, + String encoding) + throws UnsupportedEncodingException { + return encodeWord(word, charset, encoding, true); + } + + /* + * Encode the given string. The parameter 'encodingWord' should + * be true if a RFC 822 "word" token is being encoded and false if a + * RFC 822 "text" token is being encoded. This is because the + * "Q" encoding defined in RFC 2047 has more restrictions when + * encoding "word" tokens. (Sigh) + */ + private static String encodeWord(String string, String charset, + String encoding, boolean encodingWord) + throws UnsupportedEncodingException { + + // If 'string' contains only US-ASCII characters, just + // return it. + int ascii = checkAscii(string); + if (ascii == ALL_ASCII) + return string; + + // Else, apply the specified charset conversion. + String jcharset; + if (charset == null) { // use default charset + jcharset = getDefaultJavaCharset(); // the java charset + charset = getDefaultMIMECharset(); // the MIME equivalent + } else // MIME charset -> java charset + jcharset = javaCharset(charset); + + // If no transfer-encoding is specified, figure one out. + if (encoding == null) { + if (ascii != MOSTLY_NONASCII) + encoding = "Q"; + else + encoding = "B"; + } + + boolean b64; + if (encoding.equalsIgnoreCase("B")) + b64 = true; + else if (encoding.equalsIgnoreCase("Q")) + b64 = false; + else + throw new UnsupportedEncodingException( + "Unknown transfer encoding: " + encoding); + + StringBuilder outb = new StringBuilder(); // the output buffer + doEncode(string, b64, jcharset, + // As per RFC 2047, size of an encoded string should not + // exceed 75 bytes. + // 7 = size of "=?", '?', 'B'/'Q', '?', "?=" + 75 - 7 - charset.length(), // the available space + "=?" + charset + "?" + encoding + "?", // prefix + true, encodingWord, outb); + + return outb.toString(); + } + + private static void doEncode(String string, boolean b64, + String jcharset, int avail, String prefix, + boolean first, boolean encodingWord, StringBuilder buf) + throws UnsupportedEncodingException { + + // First find out what the length of the encoded version of + // 'string' would be. + byte[] bytes = string.getBytes(jcharset); + int len; + if (b64) // "B" encoding + len = BEncoderStream.encodedLength(bytes); + else // "Q" + len = QEncoderStream.encodedLength(bytes, encodingWord); + + int size; + if ((len > avail) && ((size = string.length()) > 1)) { + // If the length is greater than 'avail', split 'string' + // into two and recurse. + // Have to make sure not to split a Unicode surrogate pair. + int split = size / 2; + if (Character.isHighSurrogate(string.charAt(split-1))) + split--; + if (split > 0) + doEncode(string.substring(0, split), b64, jcharset, + avail, prefix, first, encodingWord, buf); + doEncode(string.substring(split, size), b64, jcharset, + avail, prefix, false, encodingWord, buf); + } else { + // length <= than 'avail'. Encode the given string + ByteArrayOutputStream os = new ByteArrayOutputStream(); + OutputStream eos; // the encoder + if (b64) // "B" encoding + eos = new BEncoderStream(os); + else // "Q" encoding + eos = new QEncoderStream(os, encodingWord); + + try { // do the encoding + eos.write(bytes); + eos.close(); + } catch (IOException ioex) { } + + byte[] encodedBytes = os.toByteArray(); // the encoded stuff + // Now write out the encoded (all ASCII) bytes into our + // StringBuilder + if (!first) // not the first line of this sequence + if (foldEncodedWords) + buf.append("\r\n "); // start a continuation line + else + buf.append(" "); // line will be folded later + + buf.append(prefix); + for (int i = 0; i < encodedBytes.length; i++) + buf.append((char)encodedBytes[i]); + buf.append("?="); // terminate the current sequence + } + } + + /** + * The string is parsed using the rules in RFC 2047 and RFC 2231 for + * parsing an "encoded-word". If the parse fails, a ParseException is + * thrown. Otherwise, it is transfer-decoded, and then + * charset-converted into Unicode. If the charset-conversion + * fails, an UnsupportedEncodingException is thrown.

+ * + * @param eword the encoded value + * @return the decoded word + * @exception ParseException if the string is not an + * encoded-word as per RFC 2047 and RFC 2231. + * @exception UnsupportedEncodingException if the charset + * conversion failed. + */ + public static String decodeWord(String eword) + throws ParseException, UnsupportedEncodingException { + + if (!eword.startsWith("=?")) // not an encoded word + throw new ParseException( + "encoded word does not start with \"=?\": " + eword); + + // get charset + int start = 2; int pos; + if ((pos = eword.indexOf('?', start)) == -1) + throw new ParseException( + "encoded word does not include charset: " + eword); + String charset = eword.substring(start, pos); + int lpos = charset.indexOf('*'); // RFC 2231 language specified? + if (lpos >= 0) // yes, throw it away + charset = charset.substring(0, lpos); + charset = javaCharset(charset); + + // get encoding + start = pos+1; + if ((pos = eword.indexOf('?', start)) == -1) + throw new ParseException( + "encoded word does not include encoding: " + eword); + String encoding = eword.substring(start, pos); + + // get encoded-sequence + start = pos+1; + if ((pos = eword.indexOf("?=", start)) == -1) + throw new ParseException( + "encoded word does not end with \"?=\": " + eword); + /* + * XXX - should include this, but leaving it out for compatibility... + * + if (decodeStrict && pos != eword.length() - 2) + throw new ParseException( + "encoded word does not end with \"?=\": " + eword);); + */ + String word = eword.substring(start, pos); + + try { + String decodedWord; + if (word.length() > 0) { + // Extract the bytes from word + ByteArrayInputStream bis = + new ByteArrayInputStream(ASCIIUtility.getBytes(word)); + + // Get the appropriate decoder + InputStream is; + if (encoding.equalsIgnoreCase("B")) + is = new BASE64DecoderStream(bis); + else if (encoding.equalsIgnoreCase("Q")) + is = new QDecoderStream(bis); + else + throw new UnsupportedEncodingException( + "unknown encoding: " + encoding); + + // For b64 & q, size of decoded word <= size of word. So + // the decoded bytes must fit into the 'bytes' array. This + // is certainly more efficient than writing bytes into a + // ByteArrayOutputStream and then pulling out the byte[] + // from it. + int count = bis.available(); + byte[] bytes = new byte[count]; + // count is set to the actual number of decoded bytes + count = is.read(bytes, 0, count); + + // Finally, convert the decoded bytes into a String using + // the specified charset + decodedWord = count <= 0 ? "" : + new String(bytes, 0, count, charset); + } else { + // no characters to decode, return empty string + decodedWord = ""; + } + if (pos + 2 < eword.length()) { + // there's still more text in the string + String rest = eword.substring(pos + 2); + if (!decodeStrict) + rest = decodeInnerWords(rest); + decodedWord += rest; + } + return decodedWord; + } catch (UnsupportedEncodingException uex) { + // explicitly catch and rethrow this exception, otherwise + // the below IOException catch will swallow this up! + throw uex; + } catch (IOException ioex) { + // Shouldn't happen. + throw new ParseException(ioex.toString()); + } catch (IllegalArgumentException iex) { + /* An unknown charset of the form ISO-XXX-XXX, will cause + * the JDK to throw an IllegalArgumentException ... Since the + * JDK will attempt to create a classname using this string, + * but valid classnames must not contain the character '-', + * and this results in an IllegalArgumentException, rather than + * the expected UnsupportedEncodingException. Yikes + */ + throw new UnsupportedEncodingException(charset); + } + } + + /** + * Look for encoded words within a word. The MIME spec doesn't + * allow this, but many broken mailers, especially Japanese mailers, + * produce such incorrect encodings. + */ + private static String decodeInnerWords(String word) + throws UnsupportedEncodingException { + int start = 0, i; + StringBuilder buf = new StringBuilder(); + while ((i = word.indexOf("=?", start)) >= 0) { + buf.append(word.substring(start, i)); + // find first '?' after opening '=?' - end of charset + int end = word.indexOf('?', i + 2); + if (end < 0) + break; + // find next '?' after that - end of encoding + end = word.indexOf('?', end + 1); + if (end < 0) + break; + // find terminating '?=' + end = word.indexOf("?=", end + 1); + if (end < 0) + break; + String s = word.substring(i, end + 2); + try { + s = decodeWord(s); + } catch (ParseException pex) { + // ignore it, just use the original string + } + buf.append(s); + start = end + 2; + } + if (start == 0) + return word; + if (start < word.length()) + buf.append(word.substring(start)); + return buf.toString(); + } + + /** + * A utility method to quote a word, if the word contains any + * characters from the specified 'specials' list.

+ * + * The HeaderTokenizer class defines two special + * sets of delimiters - MIME and RFC 822.

+ * + * This method is typically used during the generation of + * RFC 822 and MIME header fields. + * + * @param word word to be quoted + * @param specials the set of special characters + * @return the possibly quoted word + * @see javax.mail.internet.HeaderTokenizer#MIME + * @see javax.mail.internet.HeaderTokenizer#RFC822 + */ + public static String quote(String word, String specials) { + int len = word == null ? 0 : word.length(); + if (len == 0) + return "\"\""; // an empty string is handled specially + + /* + * Look for any "bad" characters, Escape and + * quote the entire string if necessary. + */ + boolean needQuoting = false; + for (int i = 0; i < len; i++) { + char c = word.charAt(i); + if (c == '"' || c == '\\' || c == '\r' || c == '\n') { + // need to escape them and then quote the whole string + StringBuilder sb = new StringBuilder(len + 3); + sb.append('"'); + sb.append(word.substring(0, i)); + int lastc = 0; + for (int j = i; j < len; j++) { + char cc = word.charAt(j); + if ((cc == '"') || (cc == '\\') || + (cc == '\r') || (cc == '\n')) + if (cc == '\n' && lastc == '\r') + ; // do nothing, CR was already escaped + else + sb.append('\\'); // Escape the character + sb.append(cc); + lastc = cc; + } + sb.append('"'); + return sb.toString(); + } else if (c < 040 || (c >= 0177 && !allowUtf8) || + specials.indexOf(c) >= 0) + // These characters cause the string to be quoted + needQuoting = true; + } + + if (needQuoting) { + StringBuilder sb = new StringBuilder(len + 2); + sb.append('"').append(word).append('"'); + return sb.toString(); + } else + return word; + } + + /** + * Fold a string at linear whitespace so that each line is no longer + * than 76 characters, if possible. If there are more than 76 + * non-whitespace characters consecutively, the string is folded at + * the first whitespace after that sequence. The parameter + * used indicates how many characters have been used in + * the current line; it is usually the length of the header name.

+ * + * Note that line breaks in the string aren't escaped; they probably + * should be. + * + * @param used characters used in line so far + * @param s the string to fold + * @return the folded string + * @since JavaMail 1.4 + */ + public static String fold(int used, String s) { + if (!foldText) + return s; + + int end; + char c; + // Strip trailing spaces and newlines + for (end = s.length() - 1; end >= 0; end--) { + c = s.charAt(end); + if (c != ' ' && c != '\t' && c != '\r' && c != '\n') + break; + } + if (end != s.length() - 1) + s = s.substring(0, end + 1); + + // if the string fits now, just return it + if (used + s.length() <= 76) + return makesafe(s); + + // have to actually fold the string + StringBuilder sb = new StringBuilder(s.length() + 4); + char lastc = 0; + while (used + s.length() > 76) { + int lastspace = -1; + for (int i = 0; i < s.length(); i++) { + if (lastspace != -1 && used + i > 76) + break; + c = s.charAt(i); + if (c == ' ' || c == '\t') + if (!(lastc == ' ' || lastc == '\t')) + lastspace = i; + lastc = c; + } + if (lastspace == -1) { + // no space, use the whole thing + sb.append(s); + s = ""; + used = 0; + break; + } + sb.append(s.substring(0, lastspace)); + sb.append("\r\n"); + lastc = s.charAt(lastspace); + sb.append(lastc); + s = s.substring(lastspace + 1); + used = 1; + } + sb.append(s); + return makesafe(sb); + } + + /** + * If the String or StringBuilder has any embedded newlines, + * make sure they're followed by whitespace, to prevent header + * injection errors. + */ + private static String makesafe(CharSequence s) { + int i; + for (i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\r' || c == '\n') + break; + } + if (i == s.length()) // went through whole string with no CR or LF + return s.toString(); + + // read the lines in the string and reassemble them, + // eliminating blank lines and inserting whitespace as necessary + StringBuilder sb = new StringBuilder(s.length() + 1); + BufferedReader r = new BufferedReader(new StringReader(s.toString())); + String line; + try { + while ((line = r.readLine()) != null) { + if (line.trim().length() == 0) + continue; // ignore empty lines + if (sb.length() > 0) { + sb.append("\r\n"); + assert line.length() > 0; // proven above + char c = line.charAt(0); + if (c != ' ' && c != '\t') + sb.append(' '); + } + sb.append(line); + } + } catch (IOException ex) { + // XXX - should never happen when reading from a string + return s.toString(); + } + return sb.toString(); + } + + /** + * Unfold a folded header. Any line breaks that aren't escaped and + * are followed by whitespace are removed. + * + * @param s the string to unfold + * @return the unfolded string + * @since JavaMail 1.4 + */ + public static String unfold(String s) { + if (!foldText) + return s; + + StringBuilder sb = null; + int i; + while ((i = indexOfAny(s, "\r\n")) >= 0) { + int start = i; + int slen = s.length(); + i++; // skip CR or NL + if (i < slen && s.charAt(i - 1) == '\r' && s.charAt(i) == '\n') + i++; // skip LF + if (start > 0 && s.charAt(start - 1) == '\\') { + // there's a backslash before the line break + // strip it out, but leave in the line break + if (sb == null) + sb = new StringBuilder(s.length()); + sb.append(s.substring(0, start - 1)); + sb.append(s.substring(start, i)); + s = s.substring(i); + } else { + char c; + // if next line starts with whitespace, + // or at the end of the string, remove the line break + // XXX - next line should always start with whitespace + if (i >= slen || (c = s.charAt(i)) == ' ' || c == '\t') { + if (sb == null) + sb = new StringBuilder(s.length()); + sb.append(s.substring(0, start)); + s = s.substring(i); + } else { + // it's not a continuation line, just leave in the newline + if (sb == null) + sb = new StringBuilder(s.length()); + sb.append(s.substring(0, i)); + s = s.substring(i); + } + } + } + if (sb != null) { + sb.append(s); + return sb.toString(); + } else + return s; + } + + /** + * Return the first index of any of the characters in "any" in "s", + * or -1 if none are found. + * + * This should be a method on String. + */ + private static int indexOfAny(String s, String any) { + return indexOfAny(s, any, 0); + } + + private static int indexOfAny(String s, String any, int start) { + try { + int len = s.length(); + for (int i = start; i < len; i++) { + if (any.indexOf(s.charAt(i)) >= 0) + return i; + } + return -1; + } catch (StringIndexOutOfBoundsException e) { + return -1; + } + } + + /** + * Convert a MIME charset name into a valid Java charset name.

+ * + * @param charset the MIME charset name + * @return the Java charset equivalent. If a suitable mapping is + * not available, the passed in charset is itself returned. + */ + public static String javaCharset(String charset) { + if (mime2java == null || charset == null) + // no mapping table, or charset parameter is null + return charset; + + String alias = mime2java.get(charset.toLowerCase(Locale.ENGLISH)); + if (alias != null) { + // verify that the mapped name is valid before trying to use it + try { + Charset.forName(alias); + } catch (Exception ex) { + alias = null; // charset alias not valid, use original name + } + } + return alias == null ? charset : alias; + } + + /** + * Convert a java charset into its MIME charset name.

+ * + * Note that a future version of JDK (post 1.2) might provide + * this functionality, in which case, we may deprecate this + * method then. + * + * @param charset the JDK charset + * @return the MIME/IANA equivalent. If a mapping + * is not possible, the passed in charset itself + * is returned. + * @since JavaMail 1.1 + */ + public static String mimeCharset(String charset) { + if (java2mime == null || charset == null) + // no mapping table or charset param is null + return charset; + + String alias = java2mime.get(charset.toLowerCase(Locale.ENGLISH)); + return alias == null ? charset : alias; + } + + private static String defaultJavaCharset; + private static String defaultMIMECharset; + + /** + * Get the default charset corresponding to the system's current + * default locale. If the System property mail.mime.charset + * is set, a system charset corresponding to this MIME charset will be + * returned.

+ * + * @return the default charset of the system's default locale, + * as a Java charset. (NOT a MIME charset) + * @since JavaMail 1.1 + */ + public static String getDefaultJavaCharset() { + if (defaultJavaCharset == null) { + /* + * If mail.mime.charset is set, it controls the default + * Java charset as well. + */ + String mimecs = null; + try { + mimecs = System.getProperty("mail.mime.charset"); + } catch (SecurityException ex) { } // ignore it + if (mimecs != null && mimecs.length() > 0) { + defaultJavaCharset = javaCharset(mimecs); + return defaultJavaCharset; + } + + try { + defaultJavaCharset = System.getProperty("file.encoding", + "8859_1"); + } catch (SecurityException sex) { + + class NullInputStream extends InputStream { + @Override + public int read() { + return 0; + } + } + InputStreamReader reader = + new InputStreamReader(new NullInputStream()); + defaultJavaCharset = reader.getEncoding(); + if (defaultJavaCharset == null) + defaultJavaCharset = "8859_1"; + } + } + + return defaultJavaCharset; + } + + /* + * Get the default MIME charset for this locale. + */ + static String getDefaultMIMECharset() { + if (defaultMIMECharset == null) { + try { + defaultMIMECharset = System.getProperty("mail.mime.charset"); + } catch (SecurityException ex) { } // ignore it + } + if (defaultMIMECharset == null) + defaultMIMECharset = mimeCharset(getDefaultJavaCharset()); + return defaultMIMECharset; + } + + // Tables to map MIME charset names to Java names and vice versa. + // XXX - Should eventually use J2SE 1.4 java.nio.charset.Charset + private static Map mime2java; + private static Map java2mime; + + static { + java2mime = new HashMap<>(40); + mime2java = new HashMap<>(14); + + try { + // Use this class's classloader to load the mapping file + // XXX - we should use SecuritySupport, but it's in another package + InputStream is = + javax.mail.internet.MimeUtility.class.getResourceAsStream( + "/META-INF/javamail.charset.map"); + + if (is != null) { + try { + is = new LineInputStream(is); + + // Load the JDK-to-MIME charset mapping table + loadMappings((LineInputStream)is, java2mime); + + // Load the MIME-to-JDK charset mapping table + loadMappings((LineInputStream)is, mime2java); + } finally { + try { + is.close(); + } catch (Exception cex) { + // ignore + } + } + } + } catch (Exception ex) { } + + // If we didn't load the tables, e.g., because we didn't have + // permission, load them manually. The entries here should be + // the same as the default javamail.charset.map. + if (java2mime.isEmpty()) { + java2mime.put("8859_1", "ISO-8859-1"); + java2mime.put("iso8859_1", "ISO-8859-1"); + java2mime.put("iso8859-1", "ISO-8859-1"); + + java2mime.put("8859_2", "ISO-8859-2"); + java2mime.put("iso8859_2", "ISO-8859-2"); + java2mime.put("iso8859-2", "ISO-8859-2"); + + java2mime.put("8859_3", "ISO-8859-3"); + java2mime.put("iso8859_3", "ISO-8859-3"); + java2mime.put("iso8859-3", "ISO-8859-3"); + + java2mime.put("8859_4", "ISO-8859-4"); + java2mime.put("iso8859_4", "ISO-8859-4"); + java2mime.put("iso8859-4", "ISO-8859-4"); + + java2mime.put("8859_5", "ISO-8859-5"); + java2mime.put("iso8859_5", "ISO-8859-5"); + java2mime.put("iso8859-5", "ISO-8859-5"); + + java2mime.put("8859_6", "ISO-8859-6"); + java2mime.put("iso8859_6", "ISO-8859-6"); + java2mime.put("iso8859-6", "ISO-8859-6"); + + java2mime.put("8859_7", "ISO-8859-7"); + java2mime.put("iso8859_7", "ISO-8859-7"); + java2mime.put("iso8859-7", "ISO-8859-7"); + + java2mime.put("8859_8", "ISO-8859-8"); + java2mime.put("iso8859_8", "ISO-8859-8"); + java2mime.put("iso8859-8", "ISO-8859-8"); + + java2mime.put("8859_9", "ISO-8859-9"); + java2mime.put("iso8859_9", "ISO-8859-9"); + java2mime.put("iso8859-9", "ISO-8859-9"); + + java2mime.put("sjis", "Shift_JIS"); + java2mime.put("jis", "ISO-2022-JP"); + java2mime.put("iso2022jp", "ISO-2022-JP"); + java2mime.put("euc_jp", "euc-jp"); + java2mime.put("koi8_r", "koi8-r"); + java2mime.put("euc_cn", "euc-cn"); + java2mime.put("euc_tw", "euc-tw"); + java2mime.put("euc_kr", "euc-kr"); + } + if (mime2java.isEmpty()) { + mime2java.put("iso-2022-cn", "ISO2022CN"); + mime2java.put("iso-2022-kr", "ISO2022KR"); + mime2java.put("utf-8", "UTF8"); + mime2java.put("utf8", "UTF8"); + mime2java.put("ja_jp.iso2022-7", "ISO2022JP"); + mime2java.put("ja_jp.eucjp", "EUCJIS"); + mime2java.put("euc-kr", "KSC5601"); + mime2java.put("euckr", "KSC5601"); + mime2java.put("us-ascii", "ISO-8859-1"); + mime2java.put("x-us-ascii", "ISO-8859-1"); + mime2java.put("gb2312", "GB18030"); + mime2java.put("cp936", "GB18030"); + mime2java.put("ms936", "GB18030"); + mime2java.put("gbk", "GB18030"); + } + } + + private static void loadMappings(LineInputStream is, + Map table) { + String currLine; + + while (true) { + try { + currLine = is.readLine(); + } catch (IOException ioex) { + break; // error in reading, stop + } + + if (currLine == null) // end of file, stop + break; + if (currLine.startsWith("--") && currLine.endsWith("--")) + // end of this table + break; + + // ignore empty lines and comments + if (currLine.trim().length() == 0 || currLine.startsWith("#")) + continue; + + // A valid entry is of the form + // where, := SPACE | HT. Parse this + StringTokenizer tk = new StringTokenizer(currLine, " \t"); + try { + String key = tk.nextToken(); + String value = tk.nextToken(); + table.put(key.toLowerCase(Locale.ENGLISH), value); + } catch (NoSuchElementException nex) { } + } + } + + static final int ALL_ASCII = 1; + static final int MOSTLY_ASCII = 2; + static final int MOSTLY_NONASCII = 3; + + /** + * Check if the given string contains non US-ASCII characters. + * @param s string + * @return ALL_ASCII if all characters in the string + * belong to the US-ASCII charset. MOSTLY_ASCII + * if more than half of the available characters + * are US-ASCII characters. Else MOSTLY_NONASCII. + */ + static int checkAscii(String s) { + int ascii = 0, non_ascii = 0; + int l = s.length(); + + for (int i = 0; i < l; i++) { + if (nonascii((int)s.charAt(i))) // non-ascii + non_ascii++; + else + ascii++; + } + + if (non_ascii == 0) + return ALL_ASCII; + if (ascii > non_ascii) + return MOSTLY_ASCII; + + return MOSTLY_NONASCII; + } + + /** + * Check if the given byte array contains non US-ASCII characters. + * @param b byte array + * @return ALL_ASCII if all characters in the string + * belong to the US-ASCII charset. MOSTLY_ASCII + * if more than half of the available characters + * are US-ASCII characters. Else MOSTLY_NONASCII. + * + * XXX - this method is no longer used + */ + static int checkAscii(byte[] b) { + int ascii = 0, non_ascii = 0; + + for (int i=0; i < b.length; i++) { + // The '&' operator automatically causes b[i] to be promoted + // to an int, and we mask out the higher bytes in the int + // so that the resulting value is not a negative integer. + if (nonascii(b[i] & 0xff)) // non-ascii + non_ascii++; + else + ascii++; + } + + if (non_ascii == 0) + return ALL_ASCII; + if (ascii > non_ascii) + return MOSTLY_ASCII; + + return MOSTLY_NONASCII; + } + + /** + * Check if the given input stream contains non US-ASCII characters. + * Upto max bytes are checked. If max is + * set to ALL, then all the bytes available in this + * input stream are checked. If breakOnNonAscii is true + * the check terminates when the first non-US-ASCII character is + * found and MOSTLY_NONASCII is returned. Else, the check continues + * till max bytes or till the end of stream. + * + * @param is the input stream + * @param max maximum bytes to check for. The special value + * ALL indicates that all the bytes in this input + * stream must be checked. + * @param breakOnNonAscii if true, then terminate the + * the check when the first non-US-ASCII character + * is found. + * @return ALL_ASCII if all characters in the string + * belong to the US-ASCII charset. MOSTLY_ASCII + * if more than half of the available characters + * are US-ASCII characters. Else MOSTLY_NONASCII. + */ + static int checkAscii(InputStream is, int max, boolean breakOnNonAscii) { + int ascii = 0, non_ascii = 0; + int len; + int block = 4096; + int linelen = 0; + boolean longLine = false, badEOL = false; + boolean checkEOL = encodeEolStrict && breakOnNonAscii; + byte buf[] = null; + if (max != 0) { + block = (max == ALL) ? 4096 : Math.min(max, 4096); + buf = new byte[block]; + } + while (max != 0) { + try { + if ((len = is.read(buf, 0, block)) == -1) + break; + int lastb = 0; + for (int i = 0; i < len; i++) { + // The '&' operator automatically causes b[i] to + // be promoted to an int, and we mask out the higher + // bytes in the int so that the resulting value is + // not a negative integer. + int b = buf[i] & 0xff; + if (checkEOL && + ((lastb == '\r' && b != '\n') || + (lastb != '\r' && b == '\n'))) + badEOL = true; + if (b == '\r' || b == '\n') + linelen = 0; + else { + linelen++; + if (linelen > 998) // 1000 - CRLF + longLine = true; + } + if (nonascii(b)) { // non-ascii + if (breakOnNonAscii) // we are done + return MOSTLY_NONASCII; + else + non_ascii++; + } else + ascii++; + lastb = b; + } + } catch (IOException ioex) { + break; + } + if (max != ALL) + max -= len; + } + + if (max == 0 && breakOnNonAscii) + // We have been told to break on the first non-ascii character. + // We haven't got any non-ascii character yet, but then we + // have not checked all of the available bytes either. So we + // cannot say for sure that this input stream is ALL_ASCII, + // and hence we must play safe and return MOSTLY_NONASCII + + return MOSTLY_NONASCII; + + if (non_ascii == 0) { // no non-us-ascii characters so far + // If we're looking at non-text data, and we saw CR without LF + // or vice versa, consider this mostly non-ASCII so that it + // will be base64 encoded (since the quoted-printable encoder + // doesn't encode this case properly). + if (badEOL) + return MOSTLY_NONASCII; + // if we've seen a long line, we degrade to mostly ascii + else if (longLine) + return MOSTLY_ASCII; + else + return ALL_ASCII; + } + if (ascii > non_ascii) // mostly ascii + return MOSTLY_ASCII; + return MOSTLY_NONASCII; + } + + static final boolean nonascii(int b) { + return b >= 0177 || (b < 040 && b != '\r' && b != '\n' && b != '\t'); + } +} + +/** + * An OutputStream that determines whether the data written to + * it is all ASCII, mostly ASCII, or mostly non-ASCII. + */ +class AsciiOutputStream extends OutputStream { + private boolean breakOnNonAscii; + private int ascii = 0, non_ascii = 0; + private int linelen = 0; + private boolean longLine = false; + private boolean badEOL = false; + private boolean checkEOL = false; + private int lastb = 0; + private int ret = 0; + + public AsciiOutputStream(boolean breakOnNonAscii, boolean encodeEolStrict) { + this.breakOnNonAscii = breakOnNonAscii; + checkEOL = encodeEolStrict && breakOnNonAscii; + } + + @Override + public void write(int b) throws IOException { + check(b); + } + + @Override + public void write(byte b[]) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte b[], int off, int len) throws IOException { + len += off; + for (int i = off; i < len ; i++) + check(b[i]); + } + + private final void check(int b) throws IOException { + b &= 0xff; + if (checkEOL && + ((lastb == '\r' && b != '\n') || (lastb != '\r' && b == '\n'))) + badEOL = true; + if (b == '\r' || b == '\n') + linelen = 0; + else { + linelen++; + if (linelen > 998) // 1000 - CRLF + longLine = true; + } + if (MimeUtility.nonascii(b)) { // non-ascii + non_ascii++; + if (breakOnNonAscii) { // we are done + ret = MimeUtility.MOSTLY_NONASCII; + throw new EOFException(); + } + } else + ascii++; + lastb = b; + } + + /** + * Return ASCII-ness of data stream. + */ + public int getAscii() { + if (ret != 0) + return ret; + // If we're looking at non-text data, and we saw CR without LF + // or vice versa, consider this mostly non-ASCII so that it + // will be base64 encoded (since the quoted-printable encoder + // doesn't encode this case properly). + if (badEOL) + return MimeUtility.MOSTLY_NONASCII; + else if (non_ascii == 0) { // no non-us-ascii characters so far + // if we've seen a long line, we degrade to mostly ascii + if (longLine) + return MimeUtility.MOSTLY_ASCII; + else + return MimeUtility.ALL_ASCII; + } + if (ascii > non_ascii) // mostly ascii + return MimeUtility.MOSTLY_ASCII; + return MimeUtility.MOSTLY_NONASCII; + } +} diff --git a/app/src/main/java/javax/mail/internet/NewsAddress.java b/app/src/main/java/javax/mail/internet/NewsAddress.java new file mode 100644 index 0000000000..3ca148b9e2 --- /dev/null +++ b/app/src/main/java/javax/mail/internet/NewsAddress.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; +import java.util.Locale; +import javax.mail.*; + +/** + * This class models an RFC1036 newsgroup address. + * + * @author Bill Shannon + * @author John Mani + */ + +public class NewsAddress extends Address { + + protected String newsgroup; + protected String host; // may be null + + private static final long serialVersionUID = -4203797299824684143L; + + /** + * Default constructor. + */ + public NewsAddress() { } + + /** + * Construct a NewsAddress with the given newsgroup. + * + * @param newsgroup the newsgroup + */ + public NewsAddress(String newsgroup) { + this(newsgroup, null); + } + + /** + * Construct a NewsAddress with the given newsgroup and host. + * + * @param newsgroup the newsgroup + * @param host the host + */ + public NewsAddress(String newsgroup, String host) { + // XXX - this method should throw an exception so we can report + // illegal addresses, but for now just remove whitespace + this.newsgroup = newsgroup.replaceAll("\\s+", ""); + this.host = host; + } + + /** + * Return the type of this address. The type of a NewsAddress + * is "news". + */ + @Override + public String getType() { + return "news"; + } + + /** + * Set the newsgroup. + * + * @param newsgroup the newsgroup + */ + public void setNewsgroup(String newsgroup) { + this.newsgroup = newsgroup; + } + + /** + * Get the newsgroup. + * + * @return newsgroup + */ + public String getNewsgroup() { + return newsgroup; + } + + /** + * Set the host. + * + * @param host the host + */ + public void setHost(String host) { + this.host = host; + } + + /** + * Get the host. + * + * @return host + */ + public String getHost() { + return host; + } + + /** + * Convert this address into a RFC 1036 address. + * + * @return newsgroup + */ + @Override + public String toString() { + return newsgroup; + } + + /** + * The equality operator. + */ + @Override + public boolean equals(Object a) { + if (!(a instanceof NewsAddress)) + return false; + + NewsAddress s = (NewsAddress)a; + return ((newsgroup == null && s.newsgroup == null) || + (newsgroup != null && newsgroup.equals(s.newsgroup))) && + ((host == null && s.host == null) || + (host != null && s.host != null && host.equalsIgnoreCase(s.host))); + } + + /** + * Compute a hash code for the address. + */ + @Override + public int hashCode() { + int hash = 0; + if (newsgroup != null) + hash += newsgroup.hashCode(); + if (host != null) + hash += host.toLowerCase(Locale.ENGLISH).hashCode(); + return hash; + } + + /** + * Convert the given array of NewsAddress objects into + * a comma separated sequence of address strings. The + * resulting string contains only US-ASCII characters, and + * hence is mail-safe. + * + * @param addresses array of NewsAddress objects + * @exception ClassCastException if any address object in the + * given array is not a NewsAddress objects. Note + * that this is a RuntimeException. + * @return comma separated address strings + */ + public static String toString(Address[] addresses) { + if (addresses == null || addresses.length == 0) + return null; + + StringBuilder s = + new StringBuilder(((NewsAddress)addresses[0]).toString()); + int used = s.length(); + for (int i = 1; i < addresses.length; i++) { + s.append(","); + used++; + String ng = ((NewsAddress)addresses[i]).toString(); + if (used + ng.length() > 76) { + s.append("\r\n\t"); + used = 8; + } + s.append(ng); + used += ng.length(); + } + + return s.toString(); + } + + /** + * Parse the given comma separated sequence of newsgroups into + * NewsAddress objects. + * + * @param newsgroups comma separated newsgroup string + * @return array of NewsAddress objects + * @exception AddressException if the parse failed + */ + public static NewsAddress[] parse(String newsgroups) + throws AddressException { + // XXX - verify format of newsgroup name? + StringTokenizer st = new StringTokenizer(newsgroups, ","); + List nglist = new ArrayList<>(); + while (st.hasMoreTokens()) { + String ng = st.nextToken(); + nglist.add(new NewsAddress(ng)); + } + return nglist.toArray(new NewsAddress[nglist.size()]); + } +} diff --git a/app/src/main/java/javax/mail/internet/ParameterList.java b/app/src/main/java/javax/mail/internet/ParameterList.java new file mode 100644 index 0000000000..5509b39569 --- /dev/null +++ b/app/src/main/java/javax/mail/internet/ParameterList.java @@ -0,0 +1,893 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.util.*; +import java.io.*; +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.ASCIIUtility; + +/** + * This class holds MIME parameters (attribute-value pairs). + * The mail.mime.encodeparameters and + * mail.mime.decodeparameters System properties + * control whether encoded parameters, as specified by + * RFC 2231, + * are supported. By default, such encoded parameters are + * supported.

+ * + * Also, in the current implementation, setting the System property + * mail.mime.decodeparameters.strict to "true" + * will cause a ParseException to be thrown for errors + * detected while decoding encoded parameters. By default, if any + * decoding errors occur, the original (undecoded) string is used.

+ * + * The current implementation supports the System property + * mail.mime.parameters.strict, which if set to false + * when parsing a parameter list allows parameter values + * to contain whitespace and other special characters without + * being quoted; the parameter value ends at the next semicolon. + * If set to true (the default), parameter values are required to conform + * to the MIME specification and must be quoted if they contain whitespace + * or special characters. + * + * @author John Mani + * @author Bill Shannon + */ + +public class ParameterList { + + /** + * The map of name, value pairs. + * The value object is either a String, for unencoded + * values, or a Value object, for encoded values, + * or a MultiValue object, for multi-segment parameters, + * or a LiteralValue object for strings that should not be encoded. + * + * We use a LinkedHashMap so that parameters are (as much as + * possible) kept in the original order. Note however that + * multi-segment parameters (see below) will appear in the + * position of the first seen segment and orphan segments + * will all move to the end. + */ + // keep parameters in order + private Map list = new LinkedHashMap<>(); + + /** + * A set of names for multi-segment parameters that we + * haven't processed yet. Normally such names are accumulated + * during the inital parse and processed at the end of the parse, + * but such names can also be set via the set method when the + * IMAP provider accumulates pre-parsed pieces of a parameter list. + * (A special call to the set method tells us when the IMAP provider + * is done setting parameters.) + * + * A multi-segment parameter is defined by RFC 2231. For example, + * "title*0=part1; title*1=part2", which represents a parameter + * named "title" with value "part1part2". + * + * Note also that each segment of the value might or might not be + * encoded, indicated by a trailing "*" on the parameter name. + * If any segment is encoded, the first segment must be encoded. + * Only the first segment contains the charset and language + * information needed to decode any encoded segments. + * + * RFC 2231 introduces many possible failure modes, which we try + * to handle as gracefully as possible. Generally, a failure to + * decode a parameter value causes the non-decoded parameter value + * to be used instead. Missing segments cause all later segments + * to be appear as independent parameters with names that include + * the segment number. For example, "title*0=part1; title*1=part2; + * title*3=part4" appears as two parameters named "title" and "title*3". + */ + private Set multisegmentNames; + + /** + * A map containing the segments for all not-yet-processed + * multi-segment parameters. The map is indexed by "name*seg". + * The value object is either a String or a Value object. + * The Value object is not decoded during the initial parse + * because the segments may appear in any order and until the + * first segment appears we don't know what charset to use to + * decode the encoded segments. The segments are hex decoded + * in order, combined into a single byte array, and converted + * to a String using the specified charset in the + * combineMultisegmentNames method. + */ + private Map slist; + + /** + * MWB 3BView: The name of the last parameter added to the map. + * Used for the AppleMail hack. + */ + private String lastName = null; + + private static final boolean encodeParameters = + PropUtil.getBooleanSystemProperty("mail.mime.encodeparameters", true); + private static final boolean decodeParameters = + PropUtil.getBooleanSystemProperty("mail.mime.decodeparameters", true); + private static final boolean decodeParametersStrict = + PropUtil.getBooleanSystemProperty( + "mail.mime.decodeparameters.strict", false); + private static final boolean applehack = + PropUtil.getBooleanSystemProperty("mail.mime.applefilenames", false); + private static final boolean windowshack = + PropUtil.getBooleanSystemProperty("mail.mime.windowsfilenames", false); + private static final boolean parametersStrict = + PropUtil.getBooleanSystemProperty("mail.mime.parameters.strict", true); + private static final boolean splitLongParameters = + PropUtil.getBooleanSystemProperty( + "mail.mime.splitlongparameters", true); + + + /** + * A struct to hold an encoded value. + * A parsed encoded value is stored as both the + * decoded value and the original encoded value + * (so that toString will produce the same result). + * An encoded value that is set explicitly is stored + * as the original value and the encoded value, to + * ensure that get will return the same value that + * was set. + */ + private static class Value { + String value; + String charset; + String encodedValue; + } + + /** + * A struct to hold a literal value that shouldn't be further encoded. + */ + private static class LiteralValue { + String value; + } + + /** + * A struct for a multi-segment parameter. Each entry in the + * List is either a String or a Value object. When all the + * segments are present and combined in the combineMultisegmentNames + * method, the value field contains the combined and decoded value. + * Until then the value field contains an empty string as a placeholder. + */ + private static class MultiValue extends ArrayList { + // keep lint happy + private static final long serialVersionUID = 699561094618751023L; + + String value; + } + + /** + * Map the LinkedHashMap's keySet iterator to an Enumeration. + */ + private static class ParamEnum implements Enumeration { + private Iterator it; + + ParamEnum(Iterator it) { + this.it = it; + } + + @Override + public boolean hasMoreElements() { + return it.hasNext(); + } + + @Override + public String nextElement() { + return it.next(); + } + } + + /** + * No-arg Constructor. + */ + public ParameterList() { + // initialize other collections only if they'll be needed + if (decodeParameters) { + multisegmentNames = new HashSet<>(); + slist = new HashMap<>(); + } + } + + /** + * Constructor that takes a parameter-list string. The String + * is parsed and the parameters are collected and stored internally. + * A ParseException is thrown if the parse fails. + * Note that an empty parameter-list string is valid and will be + * parsed into an empty ParameterList. + * + * @param s the parameter-list string. + * @exception ParseException if the parse fails. + */ + public ParameterList(String s) throws ParseException { + this(); + + HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME); + for (;;) { + HeaderTokenizer.Token tk = h.next(); + int type = tk.getType(); + String name, value; + + if (type == HeaderTokenizer.Token.EOF) // done + break; + + if ((char)type == ';') { + // expect parameter name + tk = h.next(); + // tolerate trailing semicolon, even though it violates the spec + if (tk.getType() == HeaderTokenizer.Token.EOF) + break; + // parameter name must be a MIME Atom + if (tk.getType() != HeaderTokenizer.Token.ATOM) + throw new ParseException("In parameter list <" + s + ">" + + ", expected parameter name, " + + "got \"" + tk.getValue() + "\""); + name = tk.getValue().toLowerCase(Locale.ENGLISH); + + // expect '=' + tk = h.next(); + if ((char)tk.getType() != '=') + throw new ParseException("In parameter list <" + s + ">" + + ", expected '=', " + + "got \"" + tk.getValue() + "\""); + + // expect parameter value + if (windowshack && + (name.equals("name") || name.equals("filename"))) + tk = h.next(';', true); + else if (parametersStrict) + tk = h.next(); + else + tk = h.next(';'); + type = tk.getType(); + // parameter value must be a MIME Atom or Quoted String + if (type != HeaderTokenizer.Token.ATOM && + type != HeaderTokenizer.Token.QUOTEDSTRING) + throw new ParseException("In parameter list <" + s + ">" + + ", expected parameter value, " + + "got \"" + tk.getValue() + "\""); + + value = tk.getValue(); + lastName = name; + if (decodeParameters) + putEncodedName(name, value); + else + list.put(name, value); + } else { + // MWB 3BView new code to add in filenames generated by + // AppleMail. + // Note - one space is assumed between name elements. + // This may not be correct but it shouldn't matter too much. + // Note: AppleMail encodes filenames with non-ascii characters + // correctly, so we don't need to worry about the name* subkeys. + if (type == HeaderTokenizer.Token.ATOM && lastName != null && + ((applehack && + (lastName.equals("name") || + lastName.equals("filename"))) || + !parametersStrict) + ) { + // Add value to previous value + String lastValue = (String)list.get(lastName); + value = lastValue + " " + tk.getValue(); + list.put(lastName, value); + } else { + throw new ParseException("In parameter list <" + s + ">" + + ", expected ';', got \"" + + tk.getValue() + "\""); + } + } + } + + if (decodeParameters) { + /* + * After parsing all the parameters, combine all the + * multi-segment parameter values together. + */ + combineMultisegmentNames(false); + } + } + + /** + * Normal users of this class will use simple parameter names. + * In some cases, for example, when processing IMAP protocol + * messages, individual segments of a multi-segment name + * (specified by RFC 2231) will be encountered and passed to + * the {@link #set} method. After all these segments are added + * to this ParameterList, they need to be combined to represent + * the logical parameter name and value. This method will combine + * all segments of multi-segment names.

+ * + * Normal users should never need to call this method. + * + * @since JavaMail 1.5 + */ + public void combineSegments() { + /* + * If we've accumulated any multi-segment names from calls to + * the set method from (e.g.) the IMAP provider, combine the pieces. + * Ignore any parse errors (e.g., from decoding the values) + * because it's too late to report them. + */ + if (decodeParameters && multisegmentNames.size() > 0) { + try { + combineMultisegmentNames(true); + } catch (ParseException pex) { + // too late to do anything about it + } + } + } + + /** + * If the name is an encoded or multi-segment name (or both) + * handle it appropriately, storing the appropriate String + * or Value object. Multi-segment names are stored in the + * main parameter list as an emtpy string as a placeholder, + * replaced later in combineMultisegmentNames with a MultiValue + * object. This causes all pieces of the multi-segment parameter + * to appear in the position of the first seen segment of the + * parameter. + */ + private void putEncodedName(String name, String value) + throws ParseException { + int star = name.indexOf('*'); + if (star < 0) { + // single parameter, unencoded value + list.put(name, value); + } else if (star == name.length() - 1) { + // single parameter, encoded value + name = name.substring(0, star); + Value v = extractCharset(value); + try { + v.value = decodeBytes(v.value, v.charset); + } catch (UnsupportedEncodingException ex) { + if (decodeParametersStrict) + throw new ParseException(ex.toString()); + } + list.put(name, v); + } else { + // multiple segments + String rname = name.substring(0, star); + multisegmentNames.add(rname); + list.put(rname, ""); + + Object v; + if (name.endsWith("*")) { + // encoded value + if (name.endsWith("*0*")) { // first segment + v = extractCharset(value); + } else { + v = new Value(); + ((Value)v).encodedValue = value; + ((Value)v).value = value; // default; decoded later + } + name = name.substring(0, name.length() - 1); + } else { + // unencoded value + v = value; + } + slist.put(name, v); + } + } + + /** + * Iterate through the saved set of names of multi-segment parameters, + * for each parameter find all segments stored in the slist map, + * decode each segment as needed, combine the segments together into + * a single decoded value, and save all segments in a MultiValue object + * in the main list indexed by the parameter name. + */ + private void combineMultisegmentNames(boolean keepConsistentOnFailure) + throws ParseException { + boolean success = false; + try { + Iterator it = multisegmentNames.iterator(); + while (it.hasNext()) { + String name = it.next(); + MultiValue mv = new MultiValue(); + /* + * Now find all the segments for this name and + * decode each segment as needed. + */ + String charset = null; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + int segment; + for (segment = 0; ; segment++) { + String sname = name + "*" + segment; + Object v = slist.get(sname); + if (v == null) // out of segments + break; + mv.add(v); + try { + if (v instanceof Value) { + Value vv = (Value)v; + if (segment == 0) { + // the first segment specifies the charset + // for all other encoded segments + charset = vv.charset; + } else { + if (charset == null) { + // should never happen + multisegmentNames.remove(name); + break; + } + } + decodeBytes(vv.value, bos); + } else { + bos.write(ASCIIUtility.getBytes((String)v)); + } + } catch (IOException ex) { + // XXX - should never happen + } + slist.remove(sname); + } + if (segment == 0) { + // didn't find any segments at all + list.remove(name); + } else { + try { + if (charset != null) + charset = MimeUtility.javaCharset(charset); + if (charset == null || charset.length() == 0) + charset = MimeUtility.getDefaultJavaCharset(); + if (charset != null) + mv.value = bos.toString(charset); + else + mv.value = bos.toString(); + } catch (UnsupportedEncodingException uex) { + if (decodeParametersStrict) + throw new ParseException(uex.toString()); + // convert as if iso-8859-1 + try { + mv.value = bos.toString("iso-8859-1"); + } catch (UnsupportedEncodingException ex) { + // should never happen + } + } + list.put(name, mv); + } + } + success = true; + } finally { + /* + * If we get here because of an exception that's going to + * be thrown (success == false) from the constructor + * (keepConsistentOnFailure == false), this is all wasted effort. + */ + if (keepConsistentOnFailure || success) { + // we should never end up with anything in slist, + // but if we do, add it all to list + if (slist.size() > 0) { + // first, decode any values that we'll add to the list + Iterator sit = slist.values().iterator(); + while (sit.hasNext()) { + Object v = sit.next(); + if (v instanceof Value) { + Value vv = (Value)v; + try { + vv.value = + decodeBytes(vv.value, vv.charset); + } catch (UnsupportedEncodingException ex) { + if (decodeParametersStrict) + throw new ParseException(ex.toString()); + } + } + } + list.putAll(slist); + } + + // clear out the set of names and segments + multisegmentNames.clear(); + slist.clear(); + } + } + } + + /** + * Return the number of parameters in this list. + * + * @return number of parameters. + */ + public int size() { + return list.size(); + } + + /** + * Returns the value of the specified parameter. Note that + * parameter names are case-insensitive. + * + * @param name parameter name. + * @return Value of the parameter. Returns + * null if the parameter is not + * present. + */ + public String get(String name) { + String value; + Object v = list.get(name.trim().toLowerCase(Locale.ENGLISH)); + if (v instanceof MultiValue) + value = ((MultiValue)v).value; + else if (v instanceof LiteralValue) + value = ((LiteralValue)v).value; + else if (v instanceof Value) + value = ((Value)v).value; + else + value = (String)v; + return value; + } + + /** + * Set a parameter. If this parameter already exists, it is + * replaced by this new value. + * + * @param name name of the parameter. + * @param value value of the parameter. + */ + public void set(String name, String value) { + name = name.trim().toLowerCase(Locale.ENGLISH); + if (decodeParameters) { + try { + putEncodedName(name, value); + } catch (ParseException pex) { + // ignore it + list.put(name, value); + } + } else + list.put(name, value); + } + + /** + * Set a parameter. If this parameter already exists, it is + * replaced by this new value. If the + * mail.mime.encodeparameters System property + * is true, and the parameter value is non-ASCII, it will be + * encoded with the specified charset, as specified by RFC 2231. + * + * @param name name of the parameter. + * @param value value of the parameter. + * @param charset charset of the parameter value. + * @since JavaMail 1.4 + */ + public void set(String name, String value, String charset) { + if (encodeParameters) { + Value ev = encodeValue(value, charset); + // was it actually encoded? + if (ev != null) + list.put(name.trim().toLowerCase(Locale.ENGLISH), ev); + else + set(name, value); + } else + set(name, value); + } + + /** + * Package-private method to set a literal value that won't be + * further encoded. Used to set the filename parameter when + * "mail.mime.encodefilename" is true. + * + * @param name name of the parameter. + * @param value value of the parameter. + */ + void setLiteral(String name, String value) { + LiteralValue lv = new LiteralValue(); + lv.value = value; + list.put(name, lv); + } + + /** + * Removes the specified parameter from this ParameterList. + * This method does nothing if the parameter is not present. + * + * @param name name of the parameter. + */ + public void remove(String name) { + list.remove(name.trim().toLowerCase(Locale.ENGLISH)); + } + + /** + * Return an enumeration of the names of all parameters in this + * list. + * + * @return Enumeration of all parameter names in this list. + */ + public Enumeration getNames() { + return new ParamEnum(list.keySet().iterator()); + } + + /** + * Convert this ParameterList into a MIME String. If this is + * an empty list, an empty string is returned. + * + * @return String + */ + @Override + public String toString() { + return toString(0); + } + + /** + * Convert this ParameterList into a MIME String. If this is + * an empty list, an empty string is returned. + * + * The 'used' parameter specifies the number of character positions + * already taken up in the field into which the resulting parameter + * list is to be inserted. It's used to determine where to fold the + * resulting parameter list. + * + * @param used number of character positions already used, in + * the field into which the parameter list is to + * be inserted. + * @return String + */ + public String toString(int used) { + ToStringBuffer sb = new ToStringBuffer(used); + Iterator> e = list.entrySet().iterator(); + + while (e.hasNext()) { + Map.Entry ent = e.next(); + String name = ent.getKey(); + String value; + Object v = ent.getValue(); + if (v instanceof MultiValue) { + MultiValue vv = (MultiValue)v; + name += "*"; + for (int i = 0; i < vv.size(); i++) { + Object va = vv.get(i); + String ns; + if (va instanceof Value) { + ns = name + i + "*"; + value = ((Value)va).encodedValue; + } else { + ns = name + i; + value = (String)va; + } + sb.addNV(ns, quote(value)); + } + } else if (v instanceof LiteralValue) { + value = ((LiteralValue)v).value; + sb.addNV(name, quote(value)); + } else if (v instanceof Value) { + /* + * XXX - We could split the encoded value into multiple + * segments if it's too long, but that's more difficult. + */ + name += "*"; + value = ((Value)v).encodedValue; + sb.addNV(name, quote(value)); + } else { + value = (String)v; + /* + * If this value is "long", split it into a multi-segment + * parameter. Only do this if we've enabled RFC2231 style + * encoded parameters. + * + * Note that we check the length before quoting the value. + * Quoting might make the string longer, although typically + * not much, so we allow a little slop in the calculation. + * In the worst case, a 60 character string will turn into + * 122 characters when quoted, which is long but not + * outrageous. + */ + if (value.length() > 60 && + splitLongParameters && encodeParameters) { + int seg = 0; + name += "*"; + while (value.length() > 60) { + sb.addNV(name + seg, quote(value.substring(0, 60))); + value = value.substring(60); + seg++; + } + if (value.length() > 0) + sb.addNV(name + seg, quote(value)); + } else { + sb.addNV(name, quote(value)); + } + } + } + return sb.toString(); + } + + /** + * A special wrapper for a StringBuffer that keeps track of the + * number of characters used in a line, wrapping to a new line + * as necessary; for use by the toString method. + */ + private static class ToStringBuffer { + private int used; // keep track of how much used on current line + private StringBuilder sb = new StringBuilder(); + + public ToStringBuffer(int used) { + this.used = used; + } + + public void addNV(String name, String value) { + sb.append("; "); + used += 2; + int len = name.length() + value.length() + 1; + if (used + len > 76) { // overflows ... + sb.append("\r\n\t"); // .. start new continuation line + used = 8; // account for the starting char + } + sb.append(name).append('='); + used += name.length() + 1; + if (used + value.length() > 76) { // still overflows ... + // have to fold value + String s = MimeUtility.fold(used, value); + sb.append(s); + int lastlf = s.lastIndexOf('\n'); + if (lastlf >= 0) // always true + used += s.length() - lastlf - 1; + else + used += s.length(); + } else { + sb.append(value); + used += value.length(); + } + } + + @Override + public String toString() { + return sb.toString(); + } + } + + // Quote a parameter value token if required. + private static String quote(String value) { + return MimeUtility.quote(value, HeaderTokenizer.MIME); + } + + private static final char hex[] = { + '0','1', '2', '3', '4', '5', '6', '7', + '8','9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + + /** + * Encode a parameter value, if necessary. + * If the value is encoded, a Value object is returned. + * Otherwise, null is returned. + * XXX - Could return a MultiValue object if parameter value is too long. + */ + private static Value encodeValue(String value, String charset) { + if (MimeUtility.checkAscii(value) == MimeUtility.ALL_ASCII) + return null; // no need to encode it + + byte[] b; // charset encoded bytes from the string + try { + b = value.getBytes(MimeUtility.javaCharset(charset)); + } catch (UnsupportedEncodingException ex) { + return null; + } + StringBuffer sb = new StringBuffer(b.length + charset.length() + 2); + sb.append(charset).append("''"); + for (int i = 0; i < b.length; i++) { + char c = (char)(b[i] & 0xff); + // do we need to encode this character? + if (c <= ' ' || c >= 0x7f || c == '*' || c == '\'' || c == '%' || + HeaderTokenizer.MIME.indexOf(c) >= 0) { + sb.append('%').append(hex[c>>4]).append(hex[c&0xf]); + } else + sb.append(c); + } + Value v = new Value(); + v.charset = charset; + v.value = value; + v.encodedValue = sb.toString(); + return v; + } + + /** + * Extract charset and encoded value. + * Value will be decoded later. + */ + private static Value extractCharset(String value) throws ParseException { + Value v = new Value(); + v.value = v.encodedValue = value; + try { + int i = value.indexOf('\''); + if (i < 0) { + if (decodeParametersStrict) + throw new ParseException( + "Missing charset in encoded value: " + value); + return v; // not encoded correctly? return as is. + } + String charset = value.substring(0, i); + int li = value.indexOf('\'', i + 1); + if (li < 0) { + if (decodeParametersStrict) + throw new ParseException( + "Missing language in encoded value: " + value); + return v; // not encoded correctly? return as is. + } + // String lang = value.substring(i + 1, li); + v.value = value.substring(li + 1); + v.charset = charset; + } catch (NumberFormatException nex) { + if (decodeParametersStrict) + throw new ParseException(nex.toString()); + } catch (StringIndexOutOfBoundsException ex) { + if (decodeParametersStrict) + throw new ParseException(ex.toString()); + } + return v; + } + + /** + * Decode the encoded bytes in value using the specified charset. + */ + private static String decodeBytes(String value, String charset) + throws ParseException, UnsupportedEncodingException { + /* + * Decode the ASCII characters in value + * into an array of bytes, and then convert + * the bytes to a String using the specified + * charset. We'll never need more bytes than + * encoded characters, so use that to size the + * array. + */ + byte[] b = new byte[value.length()]; + int i, bi; + for (i = 0, bi = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '%') { + try { + String hex = value.substring(i + 1, i + 3); + c = (char)Integer.parseInt(hex, 16); + i += 2; + } catch (NumberFormatException ex) { + if (decodeParametersStrict) + throw new ParseException(ex.toString()); + } catch (StringIndexOutOfBoundsException ex) { + if (decodeParametersStrict) + throw new ParseException(ex.toString()); + } + } + b[bi++] = (byte)c; + } + if (charset != null) + charset = MimeUtility.javaCharset(charset); + if (charset == null || charset.length() == 0) + charset = MimeUtility.getDefaultJavaCharset(); + return new String(b, 0, bi, charset); + } + + /** + * Decode the encoded bytes in value and write them to the OutputStream. + */ + private static void decodeBytes(String value, OutputStream os) + throws ParseException, IOException { + /* + * Decode the ASCII characters in value + * and write them to the stream. + */ + int i; + for (i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '%') { + try { + String hex = value.substring(i + 1, i + 3); + c = (char)Integer.parseInt(hex, 16); + i += 2; + } catch (NumberFormatException ex) { + if (decodeParametersStrict) + throw new ParseException(ex.toString()); + } catch (StringIndexOutOfBoundsException ex) { + if (decodeParametersStrict) + throw new ParseException(ex.toString()); + } + } + os.write((byte)c); + } + } +} diff --git a/app/src/main/java/javax/mail/internet/ParseException.java b/app/src/main/java/javax/mail/internet/ParseException.java new file mode 100644 index 0000000000..8497be8dda --- /dev/null +++ b/app/src/main/java/javax/mail/internet/ParseException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import javax.mail.MessagingException; + +/** + * The exception thrown due to an error in parsing RFC822 + * or MIME headers, including multipart bodies. + * + * @author John Mani + */ + +public class ParseException extends MessagingException { + + private static final long serialVersionUID = 7649991205183658089L; + + /** + * Constructs a ParseException with no detail message. + */ + public ParseException() { + super(); + } + + /** + * Constructs a ParseException with the specified detail message. + * @param s the detail message + */ + public ParseException(String s) { + super(s); + } +} diff --git a/app/src/main/java/javax/mail/internet/PreencodedMimeBodyPart.java b/app/src/main/java/javax/mail/internet/PreencodedMimeBodyPart.java new file mode 100644 index 0000000000..3188ecafb2 --- /dev/null +++ b/app/src/main/java/javax/mail/internet/PreencodedMimeBodyPart.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.io.*; +import java.util.Enumeration; +import javax.mail.*; + +import com.sun.mail.util.LineOutputStream; + +/** + * A MimeBodyPart that handles data that has already been encoded. + * This class is useful when constructing a message and attaching + * data that has already been encoded (for example, using base64 + * encoding). The data may have been encoded by the application, + * or may have been stored in a file or database in encoded form. + * The encoding is supplied when this object is created. The data + * is attached to this object in the usual fashion, by using the + * setText, setContent, or + * setDataHandler methods. + * + * @since JavaMail 1.4 + */ + +public class PreencodedMimeBodyPart extends MimeBodyPart { + private String encoding; + + /** + * Create a PreencodedMimeBodyPart that assumes the data is + * encoded using the specified encoding. The encoding must + * be a MIME supported Content-Transfer-Encoding. + * + * @param encoding the Content-Transfer-Encoding + */ + public PreencodedMimeBodyPart(String encoding) { + this.encoding = encoding; + } + + /** + * Returns the content transfer encoding specified when + * this object was created. + */ + @Override + public String getEncoding() throws MessagingException { + return encoding; + } + + /** + * Output the body part as an RFC 822 format stream. + * + * @exception IOException if an error occurs writing to the + * stream or if an error is generated + * by the javax.activation layer. + * @exception MessagingException for other failures + * @see javax.activation.DataHandler#writeTo + */ + @Override + public void writeTo(OutputStream os) + throws IOException, MessagingException { + + // see if we already have a LOS + LineOutputStream los = null; + if (os instanceof LineOutputStream) { + los = (LineOutputStream) os; + } else { + los = new LineOutputStream(os); + } + + // First, write out the header + Enumeration hdrLines = getAllHeaderLines(); + while (hdrLines.hasMoreElements()) + los.writeln(hdrLines.nextElement()); + + // The CRLF separator between header and content + los.writeln(); + + // Finally, the content, already encoded. + getDataHandler().writeTo(os); + os.flush(); + } + + /** + * Force the Content-Transfer-Encoding header to use + * the encoding that was specified when this object was created. + */ + @Override + protected void updateHeaders() throws MessagingException { + super.updateHeaders(); + MimeBodyPart.setEncoding(this, encoding); + } +} diff --git a/app/src/main/java/javax/mail/internet/SharedInputStream.java b/app/src/main/java/javax/mail/internet/SharedInputStream.java new file mode 100644 index 0000000000..b5cd41919c --- /dev/null +++ b/app/src/main/java/javax/mail/internet/SharedInputStream.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.io.*; + +/** + * An InputStream that is backed by data that can be shared by multiple + * readers may implement this interface. This allows users of such an + * InputStream to determine the current position in the InputStream, and + * to create new InputStreams representing a subset of the data in the + * original InputStream. The new InputStream will access the same + * underlying data as the original, without copying the data.

+ * + * Note that implementations of this interface must ensure that the + * close method does not close any underlying stream + * that might be shared by multiple instances of SharedInputStream + * until all shared instances have been closed. + * + * @author Bill Shannon + * @since JavaMail 1.2 + */ + +public interface SharedInputStream { + /** + * Return the current position in the InputStream, as an + * offset from the beginning of the InputStream. + * + * @return the current position + */ + public long getPosition(); + + /** + * Return a new InputStream representing a subset of the data + * from this InputStream, starting at start (inclusive) + * up to end (exclusive). start must be + * non-negative. If end is -1, the new stream ends + * at the same place as this stream. The returned InputStream + * will also implement the SharedInputStream interface. + * + * @param start the starting position + * @param end the ending position + 1 + * @return the new stream + */ + public InputStream newStream(long start, long end); +} diff --git a/app/src/main/java/javax/mail/internet/UniqueValue.java b/app/src/main/java/javax/mail/internet/UniqueValue.java new file mode 100644 index 0000000000..6977a89734 --- /dev/null +++ b/app/src/main/java/javax/mail/internet/UniqueValue.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.internet; + +import java.net.*; +import java.util.concurrent.atomic.AtomicInteger; +import javax.mail.Session; + +/** + * This is a utility class that generates unique values. The generated + * String contains only US-ASCII characters and hence is safe for use + * in RFC822 headers.

+ * + * This is a package private class. + * + * @author John Mani + * @author Max Spivak + * @author Bill Shannon + */ + +class UniqueValue { + /** + * A global unique number, to ensure uniqueness of generated strings. + */ + private static AtomicInteger id = new AtomicInteger(); + + /** + * Get a unique value for use in a multipart boundary string. + * + * This implementation generates it by concatenating a global + * part number, a newly created object's hashCode(), + * and the current time (in milliseconds). + */ + public static String getUniqueBoundaryValue() { + StringBuilder s = new StringBuilder(); + long hash = s.hashCode(); + + // Unique string is ----=_Part__. + s.append("----=_Part_").append(id.getAndIncrement()).append("_"). + append(hash).append('.'). + append(System.currentTimeMillis()); + return s.toString(); + } + + /** + * Get a unique value for use in a Message-ID. + * + * This implementation generates it by concatenating a newly + * created object's hashCode(), a global ID + * (incremented on every use), the current time (in milliseconds), + * and the host name from this user's local address generated by + * InternetAddress.getLocalAddress(). + * (The host name defaults to "localhost" if + * getLocalAddress() returns null.) + * + * @param ssn Session object used to get the local address + * @see javax.mail.internet.InternetAddress + */ + public static String getUniqueMessageIDValue(Session ssn) { + String suffix = null; + + InternetAddress addr = InternetAddress.getLocalAddress(ssn); + if (addr != null) + suffix = addr.getAddress(); + else { + suffix = "jakartamailuser@localhost"; // worst-case default + } + int at = suffix.lastIndexOf('@'); + if (at >= 0) + suffix = suffix.substring(at); + + StringBuilder s = new StringBuilder(); + + // Unique string is .. + s.append(s.hashCode()).append('.'). + append(id.getAndIncrement()).append('.'). + append(System.currentTimeMillis()). + append(suffix); + return s.toString(); + } +} diff --git a/app/src/main/java/javax/mail/internet/package.html b/app/src/main/java/javax/mail/internet/package.html new file mode 100644 index 0000000000..7b8b0f3660 --- /dev/null +++ b/app/src/main/java/javax/mail/internet/package.html @@ -0,0 +1,553 @@ + + + + + + +javax.mail.internet package + + + +

+Classes specific to Internet mail systems. +This package supports features that are specific to Internet mail systems +based on the MIME standard +(RFC 2045, +RFC 2046, and +RFC 2047). +The IMAP, SMTP, and POP3 protocols use +{@link javax.mail.internet.MimeMessage MimeMessages}. +

+Properties +

+The Jakarta Mail API supports the following standard properties, +which may be set in the Session object, or in the +Properties object used to create the Session object. +The properties are always set as strings; the Type column describes +how the string is interpreted. For example, use +

+
+	session.setProperty("mail.mime.address.strict", "false");
+
+

+to set the mail.mime.address.strict property, +which is of type boolean. +

+ + + + + + + + + + + + + + + + + + + +
Jakarta Mail properties
NameTypeDescription
mail.mime.address.strictboolean +The mail.mime.address.strict session property controls +the parsing of address headers. By default, strict parsing of address +headers is done. If this property is set to "false", +strict parsing is not done and many illegal addresses that sometimes +occur in real messages are allowed. See the InternetAddress +class for details. +
mail.mime.allowutf8boolean +If set to "true", UTF-8 strings are allowed in message headers, +e.g., in addresses. This should only be set if the mail server also +supports UTF-8. +
+

+The Jakarta Mail API specification requires support for the following properties, +which must be set in the System properties. +The properties are always set as strings; the Type column describes +how the string is interpreted. For example, use +

+
+	System.setProperty("mail.mime.decodetext.strict", "false");
+
+

+to set the mail.mime.decodetext.strict property, +which is of type boolean. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Jakarta Mail System properties
NameTypeDescription
mail.mime.charsetString +The mail.mime.charset System property can +be used to specify the default MIME charset to use for encoded words +and text parts that don't otherwise specify a charset. Normally, the +default MIME charset is derived from the default Java charset, as +specified in the file.encoding System property. Most +applications will have no need to explicitly set the default MIME +charset. In cases where the default MIME charset to be used for +mail messages is different than the charset used for files stored on +the system, this property should be set. +
mail.mime.decodetext.strictboolean +The mail.mime.decodetext.strict property controls +decoding of MIME encoded words. The MIME spec requires that encoded +words start at the beginning of a whitespace separated word. Some +mailers incorrectly include encoded words in the middle of a word. +If the mail.mime.decodetext.strict System property is +set to "false", an attempt will be made to decode these +illegal encoded words. The default is true. +
mail.mime.encodeeol.strictboolean +The mail.mime.encodeeol.strict property controls the +choice of Content-Transfer-Encoding for MIME parts that are not of +type "text". Often such parts will contain textual data for which +an encoding that allows normal end of line conventions is appropriate. +In rare cases, such a part will appear to contain entirely textual +data, but will require an encoding that preserves CR and LF characters +without change. If the mail.mime.encodeeol.strict +System property is set to "true", such an encoding will +be used when necessary. The default is false. +
mail.mime.decodefilenameboolean +If set to "true", the getFileName method +uses the MimeUtility +method decodeText to decode any +non-ASCII characters in the filename. Note that this decoding +violates the MIME specification, but is useful for interoperating +with some mail clients that use this convention. +The default is false. +
mail.mime.encodefilenameboolean +If set to "true", the setFileName method +uses the MimeUtility +method encodeText to encode any +non-ASCII characters in the filename. Note that this encoding +violates the MIME specification, but is useful for interoperating +with some mail clients that use this convention. +The default is false. +
mail.mime.decodeparametersboolean +If set to "false", non-ASCII parameters in a +ParameterList, e.g., in a Content-Type header, +will not be decoded as specified by +RFC 2231. +The default is true. +
mail.mime.encodeparametersboolean +If set to "false", non-ASCII parameters in a +ParameterList, e.g., in a Content-Type header, +will not be encoded as specified by +RFC 2231. +The default is true. +
mail.mime.multipart. ignoremissingendboundaryboolean +Normally, when parsing a multipart MIME message, a message that is +missing the final end boundary line is not considered an error. +The data simply ends at the end of the input. Note that messages +of this form violate the MIME specification. If the property +mail.mime.multipart.ignoremissingendboundary is set +to false, such messages are considered an error and a +MesagingException will be thrown when parsing such a +message. +
mail.mime.multipart. ignoremissingboundaryparameterboolean +If the Content-Type header for a multipart content does not have +a boundary parameter, the multipart parsing code +will look for the first line in the content that looks like a +boundary line and extract the boundary parameter from the line. +If this property is set to "false", a +MessagingException will be thrown if the Content-Type +header doesn't specify a boundary parameter. +The default is true. +
mail.mime.multipart. ignoreexistingboundaryparameterboolean +Normally the boundary parameter in the Content-Type header of a multipart +body part is used to specify the separator between parts of the multipart +body. This System property may be set to "true" to cause +the parser to look for a line in the multipart body that looks like a +boundary line and use that value as the separator between subsequent parts. +This may be useful in cases where a broken anti-virus product has rewritten +the message incorrectly such that the boundary parameter and the actual +boundary value no longer match. +The default value of this property is false. +
mail.mime.multipart. allowemptyboolean +Normally, when writing out a MimeMultipart that contains no body +parts, or when trying to parse a multipart message with no body parts, +a MessagingException is thrown. The MIME spec does not allow +multipart content with no body parts. This +System property may be set to "true" to override this behavior. +When writing out such a MimeMultipart, a single empty part will be +included. When reading such a multipart, a MimeMultipart will be created +with no body parts. +The default value of this property is false. +
+ + +

+The following properties are supported by the EE4J implementation of +Jakarta Mail, but are not currently a required part of the specification. +These must be set as Session properties. +The names, types, defaults, and semantics of these properties may +change in future releases. +

+ + + + + + + + + + + + + + + + + + + +
Jakarta Mail implementation properties
NameTypeDescription
mail.alternatesString +A string containing other email addresses that the current user is known by. +The MimeMessage reply method will eliminate any +of these addresses from the recipient list in the message it constructs, +to avoid sending the reply back to the sender. +
mail.replyallccboolean +If set to "true", the MimeMessage +reply method will put all recipients except the original +sender in the Cc list of the newly constructed message. +Normally, recipients in the To header of the original +message will also appear in the To list of the newly +constructed message. +
+ +

+The following properties are supported by the EE4J implementation of +Jakarta Mail, but are not currently a required part of the specification. +These must be set as System properties. +The names, types, defaults, and semantics of these properties may +change in future releases. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Jakarta Mail implementation System properties
NameTypeDescription
mail.mime.base64.ignoreerrorsboolean +If set to "true", the BASE64 decoder will ignore errors +in the encoded data, returning EOF. This may be useful when dealing +with improperly encoded messages that contain extraneous data at the +end of the encoded stream. Note however that errors anywhere in the +stream will cause the decoder to stop decoding so this should be used +with extreme caution. The default is false. +
mail.mime.foldtextboolean +If set to "true", header fields containing just text +such as the Subject and Content-Description +header fields, and long parameter values in structured headers such +as Content-Type will be folded (broken into 76 character lines) +when set and unfolded when read. The default is true. +
mail.mime.setcontenttypefilenameboolean +If set to "true", the setFileName method +will also set the name parameter on the Content-Type +header to the specified filename. This supports interoperability with +some old mail clients. The default is true. +
mail.mime.setdefaulttextcharsetboolean +When updating the headers of a message, a body +part with a text content type but no charset +parameter will have a charset parameter added to it +if this property is set to "true". +The default is true. +
mail.mime.parameters.strictboolean +If set to false, when reading a message, parameter values in header fields +such as Content-Type and Content-Disposition +are allowed to contain whitespace and other special characters without +being quoted; the parameter value ends at the next semicolon. +If set to true (the default), parameter values are required to conform +to the MIME specification and must be quoted if they contain whitespace +or special characters. +
mail.mime.applefilenamesboolean +Apple Mail incorrectly encodes filenames that contain spaces, +forgetting to quote the parameter value. If this property is +set to "true", Jakarta Mail will try to detect this +situation when parsing parameters and work around it. +The default is false. +Note that this property handles a subset of the cases handled +by setting the mail.mime.parameters.strict property to false. +This property will likely be removed in a future release. +
mail.mime.windowsfilenamesboolean +Internet Explorer 6 incorrectly includes a complete pathname +in the filename parameter of the Content-Disposition header +for uploaded files, and fails to properly escape the backslashes +in the pathname. If this property is +set to "true", Jakarta Mail will preserve all backslashes +in the "filename" and "name" parameters of any MIME header. +The default is false. +Note that this is a violation of the MIME specification but may +be useful when using Jakarta Mail to parse HTTP messages for uploaded +files sent by IE6. +
mail.mime. ignoreunknownencodingboolean +If set to "true", an unknown value in the +Content-Transfer-Encoding header will be ignored +when reading a message and an encoding of "8bit" will be assumed. +If set to "false", an exception is thrown for an +unknown encoding value. The default is false. +
mail.mime.uudecode. ignoreerrorsboolean +If set to "true", errors in the encoded format of a +uuencoded document will be ignored when reading a message part. +If set to "false", an exception is thrown for an +incorrectly encoded message part. The default is false. +
mail.mime.uudecode. ignoremissingbeginendboolean +If set to "true", a missing "being" or "end" line in a +uuencoded document will be ignored when reading a message part. +If set to "false", an exception is thrown for a +uuencoded message part without the required "begin" and "end" lines. +The default is false. +
mail.mime. ignorewhitespacelinesboolean +Normally the header of a MIME part is separated from the body by an empty +line. This System property may be set to "true" to cause +the parser to consider a line containing only whitespace to be an empty +line. The default value of this property is false. +
mail.mime. ignoremultipartencodingboolean +The MIME spec does not allow body parts of type multipart/* to be encoded. +The Content-Transfer-Encoding header is ignored in this case. +Setting this System property to "false" will +cause the Content-Transfer-Encoding header to be honored for multipart +content. +The default value of this property is true. +
mail.mime.allowencodedmessagesboolean +The MIME spec does not allow body parts of type message/* to be encoded. +The Content-Transfer-Encoding header is ignored in this case. +Some versions of Microsoft Outlook will incorrectly encode message +attachments. Setting this System property to "true" will +cause the Content-Transfer-Encoding header to be honored for message +attachments. +The default value of this property is false. +
mail.mime.contenttypehandlerString +In some cases Jakarta Mail is unable to process messages with an invalid +Content-Type header. The header may have incorrect syntax or other +problems. This property specifies the name of a class that will be +used to clean up the Content-Type header value before Jakarta Mail uses it. +The class must have a method with this signature: +public static String cleanContentType(MimePart mp, String contentType) +Whenever Jakarta Mail accesses the Content-Type header of a message, it +will pass the value to this method and use the returned value instead. +The value may be null if the Content-Type header isn't present. +Returning null will cause the default Content-Type to be used. +The MimePart may be used to access other headers of the message part +to determine how to correct the Content-Type. +Note that the Content-Type handler doesn't affect the +getHeader method, which still returns the raw header value. +Note also that the handler doesn't affect the IMAP provider; the IMAP +server is responsible for returning pre-parsed, syntactically correct +Content-Type information. +
mail.mime.address.usecanonicalhostnameboolean +Use the +{@link java.net.InetAddress#getCanonicalHostName InetAddress.getCanonicalHostName} +method to determine the host name in the +{@link javax.mail.internet.InternetAddress#getLocalAddress InternetAddress.getLocalAddress} +method. +With some network configurations, InetAddress.getCanonicalHostName may be +slow or may return an address instead of a host name. +In that case, setting this System property to false will cause the +{@link java.net.InetAddress#getHostName InetAddress.getHostName} +method to be used instead. +The default is true. +
+

+The current +implementation of classes in this package log debugging information using +{@link java.util.logging.Logger} as described in the following table: +

+ + + + + + + + + + + + + +
Jakarta Mail Loggers
Logger NameLogging LevelPurpose
javax.mail.internetFINEGeneral debugging output
+ + + diff --git a/app/src/main/java/javax/mail/package.html b/app/src/main/java/javax/mail/package.html new file mode 100644 index 0000000000..ae86d2b76f --- /dev/null +++ b/app/src/main/java/javax/mail/package.html @@ -0,0 +1,350 @@ + + + + + + +javax.mail package + + + +

+The Jakarta Mail API +provides classes that model a mail system. +The javax.mail package defines classes that are common to +all mail systems. +The javax.mail.internet package defines classes that are specific +to mail systems based on internet standards such as MIME, SMTP, POP3, and IMAP. +The Jakarta Mail API includes the javax.mail package and subpackages. +

+

+For an overview of the Jakarta Mail API, read the + +Jakarta Mail specification. +

+

+The code to send a plain text message can be as simple as the following: +

+
+    Properties props = new Properties();
+    props.put("mail.smtp.host", "my-mail-server");
+    Session session = Session.getInstance(props, null);
+
+    try {
+	MimeMessage msg = new MimeMessage(session);
+	msg.setFrom("me@example.com");
+	msg.setRecipients(Message.RecipientType.TO,
+			  "you@example.com");
+	msg.setSubject("Jakarta Mail hello world example");
+	msg.setSentDate(new Date());
+	msg.setText("Hello, world!\n");
+	Transport.send(msg, "me@example.com", "my-password");
+    } catch (MessagingException mex) {
+	System.out.println("send failed, exception: " + mex);
+    }
+
+

+The Jakarta Mail download bundle contains many more complete examples +in the "demo" directory. +

+

+Don't forget to see the + +Jakarta Mail API FAQ +for answers to the most common questions. +The +Jakarta Mail web site +contains many additional resources. +

+Properties +

+The Jakarta Mail API supports the following standard properties, +which may be set in the Session object, or in the +Properties object used to create the Session object. +The properties are always set as strings; the Type column describes +how the string is interpreted. For example, use +

+
+	props.put("mail.debug", "true");
+
+

+to set the mail.debug property, which is of type boolean. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Jakarta Mail properties
NameTypeDescription
mail.debugboolean +The initial debug mode. +Default is false. +
mail.fromString +The return email address of the current user, used by the +InternetAddress method getLocalAddress. +
mail.mime.address.strictboolean +The MimeMessage class uses the InternetAddress method +parseHeader to parse headers in messages. This property +controls the strict flag passed to the parseHeader +method. The default is true. +
mail.hostString +The default host name of the mail server for both Stores and Transports. +Used if the mail.protocol.host property isn't set. +
mail.store.protocolString +Specifies the default message access protocol. The +Session method getStore() returns a Store +object that implements this protocol. By default the first Store +provider in the configuration files is returned. +
mail.transport.protocolString +Specifies the default message transport protocol. The +Session method getTransport() returns a Transport +object that implements this protocol. By default the first Transport +provider in the configuration files is returned. +
mail.userString +The default user name to use when connecting to the mail server. +Used if the mail.protocol.user property isn't set. +
mail.protocol.classString +Specifies the fully qualified class name of the provider for the +specified protocol. Used in cases where more than one provider +for a given protocol exists; this property can be used to specify +which provider to use by default. The provider must still be listed +in a configuration file. +
mail.protocol.hostString +The host name of the mail server for the specified protocol. +Overrides the mail.host property. +
mail.protocol.portint +The port number of the mail server for the specified protocol. +If not specified the protocol's default port number is used. +
mail.protocol.userString +The user name to use when connecting to mail servers +using the specified protocol. +Overrides the mail.user property. +
+ +

+The following properties are supported by the EE4J implementation of +Jakarta Mail, but are not currently a required part of the specification. +The names, types, defaults, and semantics of these properties may +change in future releases. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Jakarta Mail implementation properties
NameTypeDescription
mail.debug.authboolean +Include protocol authentication commands (including usernames and passwords) +in the debug output. +Default is false. +
mail.debug.auth.usernameboolean +Include the user name in non-protocol debug output. +Default is true. +
mail.debug.auth.passwordboolean +Include the password in non-protocol debug output. +Default is false. +
mail.transport.protocol.address-typeString +Specifies the default message transport protocol for the specified address type. +The Session method getTransport(Address) returns a +Transport object that implements this protocol when the address is of the +specified type (e.g., "rfc822" for standard internet addresses). +By default the first Transport configured for that address type is used. +This property can be used to override the behavior of the +{@link javax.mail.Transport#send send} method of the +{@link javax.mail.Transport Transport} class so that (for example) the "smtps" +protocol is used instead of the "smtp" protocol by setting the property +mail.transport.protocol.rfc822 to "smtps". +
mail.event.scopeString +Controls the scope of events. (See the javax.mail.event package.) +By default, a separate event queue and thread is used for events for each +Store, Transport, or Folder. +If this property is set to "session", all such events are put in a single +event queue processed by a single thread for the current session. +If this property is set to "application", all such events are put in a single +event queue processed by a single thread for the current application. +(Applications are distinguished by their context class loader.) +
mail.event.executorjava.util.concurrent.Executor +By default, a new Thread is created for each event queue. +This thread is used to call the listeners for these events. +If this property is set to an instance of an Executor, the +Executor.execute method is used to run the event dispatcher +for an event queue. The event dispatcher runs until the +event queue is no longer in use. +
+ +

+The Jakarta Mail API also supports several System properties; +see the {@link javax.mail.internet} package documentation +for details. +

+

+The Jakarta Mail reference +implementation includes protocol providers in subpackages of +com.sun.mail. Note that the APIs to these protocol +providers are not part of the standard Jakarta Mail API. Portable +programs will not use these APIs. +

+

+Nonportable programs may use the APIs of the protocol providers +by (for example) casting a returned Folder object to a +com.sun.mail.imap.IMAPFolder object. Similarly for +Store and Message objects returned from the +standard Jakarta Mail APIs. +

+

+The protocol providers also support properties that are specific to +those providers. The package documentation for the +{@link com.sun.mail.imap IMAP}, {@link com.sun.mail.pop3 POP3}, +and {@link com.sun.mail.smtp SMTP} packages provide details. +

+

+In addition to printing debugging output as controlled by the +{@link javax.mail.Session Session} configuration, the current +implementation of classes in this package log the same information using +{@link java.util.logging.Logger} as described in the following table: +

+ + + + + + + + + + + + + + + + + + + +
Jakarta Mail Loggers
Logger NameLogging LevelPurpose
javax.mailCONFIGConfiguration of the Session
javax.mailFINEGeneral debugging output
+ + + diff --git a/app/src/main/java/javax/mail/search/AddressStringTerm.java b/app/src/main/java/javax/mail/search/AddressStringTerm.java new file mode 100644 index 0000000000..1e48e72d8b --- /dev/null +++ b/app/src/main/java/javax/mail/search/AddressStringTerm.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; +import javax.mail.Address; +import javax.mail.internet.InternetAddress; + +/** + * This abstract class implements string comparisons for Message + * addresses.

+ * + * Note that this class differs from the AddressTerm class + * in that this class does comparisons on address strings rather than + * Address objects. + * + * @since JavaMail 1.1 + */ + +public abstract class AddressStringTerm extends StringTerm { + + private static final long serialVersionUID = 3086821234204980368L; + + /** + * Constructor. + * + * @param pattern the address pattern to be compared. + */ + protected AddressStringTerm(String pattern) { + super(pattern, true); // we need case-insensitive comparison. + } + + /** + * Check whether the address pattern specified in the constructor is + * a substring of the string representation of the given Address + * object.

+ * + * Note that if the string representation of the given Address object + * contains charset or transfer encodings, the encodings must be + * accounted for, during the match process.

+ * + * @param a The comparison is applied to this Address object. + * @return true if the match succeeds, otherwise false. + */ + protected boolean match(Address a) { + if (a instanceof InternetAddress) { + InternetAddress ia = (InternetAddress)a; + // We dont use toString() to get "a"'s String representation, + // because InternetAddress.toString() returns a RFC 2047 + // encoded string, which isn't what we need here. + + return super.match(ia.toUnicodeString()); + } else + return super.match(a.toString()); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AddressStringTerm)) + return false; + return super.equals(obj); + } +} diff --git a/app/src/main/java/javax/mail/search/AddressTerm.java b/app/src/main/java/javax/mail/search/AddressTerm.java new file mode 100644 index 0000000000..748ad33b46 --- /dev/null +++ b/app/src/main/java/javax/mail/search/AddressTerm.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Address; + +/** + * This class implements Message Address comparisons. + * + * @author Bill Shannon + * @author John Mani + */ + +public abstract class AddressTerm extends SearchTerm { + /** + * The address. + * + * @serial + */ + protected Address address; + + private static final long serialVersionUID = 2005405551929769980L; + + protected AddressTerm(Address address) { + this.address = address; + } + + /** + * Return the address to match with. + * + * @return the adddress + */ + public Address getAddress() { + return address; + } + + /** + * Match against the argument Address. + * + * @param a the address to match + * @return true if it matches + */ + protected boolean match(Address a) { + return (a.equals(address)); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AddressTerm)) + return false; + AddressTerm at = (AddressTerm)obj; + return at.address.equals(this.address); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return address.hashCode(); + } +} diff --git a/app/src/main/java/javax/mail/search/AndTerm.java b/app/src/main/java/javax/mail/search/AndTerm.java new file mode 100644 index 0000000000..d0b935d6da --- /dev/null +++ b/app/src/main/java/javax/mail/search/AndTerm.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; + +/** + * This class implements the logical AND operator on individual + * SearchTerms. + * + * @author Bill Shannon + * @author John Mani + */ +public final class AndTerm extends SearchTerm { + + /** + * The array of terms on which the AND operator should be + * applied. + * + * @serial + */ + private SearchTerm[] terms; + + private static final long serialVersionUID = -3583274505380989582L; + + /** + * Constructor that takes two terms. + * + * @param t1 first term + * @param t2 second term + */ + public AndTerm(SearchTerm t1, SearchTerm t2) { + terms = new SearchTerm[2]; + terms[0] = t1; + terms[1] = t2; + } + + /** + * Constructor that takes an array of SearchTerms. + * + * @param t array of terms + */ + public AndTerm(SearchTerm[] t) { + terms = new SearchTerm[t.length]; // clone the array + for (int i = 0; i < t.length; i++) + terms[i] = t[i]; + } + + /** + * Return the search terms. + * + * @return the search terms + */ + public SearchTerm[] getTerms() { + return terms.clone(); + } + + /** + * The AND operation.

+ * + * The terms specified in the constructor are applied to + * the given object and the AND operator is applied to their results. + * + * @param msg The specified SearchTerms are applied to this Message + * and the AND operator is applied to their results. + * @return true if the AND succeds, otherwise false + */ + @Override + public boolean match(Message msg) { + for (int i=0; i < terms.length; i++) + if (!terms[i].match(msg)) + return false; + return true; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AndTerm)) + return false; + AndTerm at = (AndTerm)obj; + if (at.terms.length != terms.length) + return false; + for (int i=0; i < terms.length; i++) + if (!terms[i].equals(at.terms[i])) + return false; + return true; + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + int hash = 0; + for (int i=0; i < terms.length; i++) + hash += terms[i].hashCode(); + return hash; + } +} diff --git a/app/src/main/java/javax/mail/search/BodyTerm.java b/app/src/main/java/javax/mail/search/BodyTerm.java new file mode 100644 index 0000000000..fa6789304e --- /dev/null +++ b/app/src/main/java/javax/mail/search/BodyTerm.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import java.io.IOException; +import javax.mail.*; + +/** + * This class implements searches on a message body. + * All parts of the message that are of MIME type "text/*" are searched. + * The pattern is a simple string that must appear as a substring in + * the message body. + * + * @author Bill Shannon + * @author John Mani + */ +public final class BodyTerm extends StringTerm { + + private static final long serialVersionUID = -4888862527916911385L; + + /** + * Constructor + * @param pattern The String to search for + */ + public BodyTerm(String pattern) { + // Note: comparison is case-insensitive + super(pattern); + } + + /** + * The match method. + * + * @param msg The pattern search is applied on this Message's body + * @return true if the pattern is found; otherwise false + */ + @Override + public boolean match(Message msg) { + return matchPart(msg); + } + + /** + * Search all the parts of the message for any text part + * that matches the pattern. + */ + private boolean matchPart(Part p) { + try { + /* + * Using isMimeType to determine the content type avoids + * fetching the actual content data until we need it. + */ + if (p.isMimeType("text/*")) { + String s = (String)p.getContent(); + if (s == null) + return false; + /* + * We invoke our superclass' (i.e., StringTerm) match method. + * Note however that StringTerm.match() is not optimized + * for substring searches in large string buffers. We really + * need to have a StringTerm subclass, say BigStringTerm, + * with its own match() method that uses a better algorithm .. + * and then subclass BodyTerm from BigStringTerm. + */ + return super.match(s); + } else if (p.isMimeType("multipart/*")) { + Multipart mp = (Multipart)p.getContent(); + int count = mp.getCount(); + for (int i = 0; i < count; i++) + if (matchPart(mp.getBodyPart(i))) + return true; + } else if (p.isMimeType("message/rfc822")) { + return matchPart((Part)p.getContent()); + } + } catch (MessagingException ex) { + } catch (IOException ex) { + } catch (RuntimeException ex) { + } + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof BodyTerm)) + return false; + return super.equals(obj); + } +} diff --git a/app/src/main/java/javax/mail/search/ComparisonTerm.java b/app/src/main/java/javax/mail/search/ComparisonTerm.java new file mode 100644 index 0000000000..57330ecd87 --- /dev/null +++ b/app/src/main/java/javax/mail/search/ComparisonTerm.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +/** + * This class models the comparison operator. This is an abstract + * class; subclasses implement comparisons for different datatypes. + * + * @author Bill Shannon + * @author John Mani + */ +public abstract class ComparisonTerm extends SearchTerm { + public static final int LE = 1; + public static final int LT = 2; + public static final int EQ = 3; + public static final int NE = 4; + public static final int GT = 5; + public static final int GE = 6; + + /** + * The comparison. + * + * @serial + */ + protected int comparison; + + private static final long serialVersionUID = 1456646953666474308L; + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ComparisonTerm)) + return false; + ComparisonTerm ct = (ComparisonTerm)obj; + return ct.comparison == this.comparison; + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return comparison; + } +} diff --git a/app/src/main/java/javax/mail/search/DateTerm.java b/app/src/main/java/javax/mail/search/DateTerm.java new file mode 100644 index 0000000000..833ad9004b --- /dev/null +++ b/app/src/main/java/javax/mail/search/DateTerm.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import java.util.Date; + +/** + * This class implements comparisons for Dates + * + * @author Bill Shannon + * @author John Mani + */ +public abstract class DateTerm extends ComparisonTerm { + /** + * The date. + * + * @serial + */ + protected Date date; + + private static final long serialVersionUID = 4818873430063720043L; + + /** + * Constructor. + * @param comparison the comparison type + * @param date The Date to be compared against + */ + protected DateTerm(int comparison, Date date) { + this.comparison = comparison; + this.date = date; + } + + /** + * Return the Date to compare with. + * + * @return the date + */ + public Date getDate() { + return new Date(date.getTime()); + } + + /** + * Return the type of comparison. + * + * @return the comparison type + */ + public int getComparison() { + return comparison; + } + + /** + * The date comparison method. + * + * @param d the date in the constructor is compared with this date + * @return true if the dates match, otherwise false + */ + protected boolean match(Date d) { + switch (comparison) { + case LE: + return d.before(date) || d.equals(date); + case LT: + return d.before(date); + case EQ: + return d.equals(date); + case NE: + return !d.equals(date); + case GT: + return d.after(date); + case GE: + return d.after(date) || d.equals(date); + default: + return false; + } + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof DateTerm)) + return false; + DateTerm dt = (DateTerm)obj; + return dt.date.equals(this.date) && super.equals(obj); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return date.hashCode() + super.hashCode(); + } +} diff --git a/app/src/main/java/javax/mail/search/FlagTerm.java b/app/src/main/java/javax/mail/search/FlagTerm.java new file mode 100644 index 0000000000..20897c6cc2 --- /dev/null +++ b/app/src/main/java/javax/mail/search/FlagTerm.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.*; + +/** + * This class implements comparisons for Message Flags. + * + * @author Bill Shannon + * @author John Mani + */ +public final class FlagTerm extends SearchTerm { + + /** + * Indicates whether to test for the presence or + * absence of the specified Flag. If true, + * then test whether all the specified flags are present, else + * test whether all the specified flags are absent. + * + * @serial + */ + private boolean set; + + /** + * Flags object containing the flags to test. + * + * @serial + */ + private Flags flags; + + private static final long serialVersionUID = -142991500302030647L; + + /** + * Constructor. + * + * @param flags Flags object containing the flags to check for + * @param set the flag setting to check for + */ + public FlagTerm(Flags flags, boolean set) { + this.flags = flags; + this.set = set; + } + + /** + * Return the Flags to test. + * + * @return the flags + */ + public Flags getFlags() { + return (Flags)flags.clone(); + } + + /** + * Return true if testing whether the flags are set. + * + * @return true if testing whether the flags are set + */ + public boolean getTestSet() { + return set; + } + + /** + * The comparison method. + * + * @param msg The flag comparison is applied to this Message + * @return true if the comparson succeeds, otherwise false. + */ + @Override + public boolean match(Message msg) { + + try { + Flags f = msg.getFlags(); + if (set) { // This is easy + if (f.contains(flags)) + return true; + else + return false; + } + + // Return true if ALL flags in the passed in Flags + // object are NOT set in this Message. + + // Got to do this the hard way ... + Flags.Flag[] sf = flags.getSystemFlags(); + + // Check each flag in the passed in Flags object + for (int i = 0; i < sf.length; i++) { + if (f.contains(sf[i])) + // this flag IS set in this Message, get out. + return false; + } + + String[] s = flags.getUserFlags(); + + // Check each flag in the passed in Flags object + for (int i = 0; i < s.length; i++) { + if (f.contains(s[i])) + // this flag IS set in this Message, get out. + return false; + } + + return true; + + } catch (MessagingException e) { + return false; + } catch (RuntimeException e) { + return false; + } + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FlagTerm)) + return false; + FlagTerm ft = (FlagTerm)obj; + return ft.set == this.set && ft.flags.equals(this.flags); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return set ? flags.hashCode() : ~flags.hashCode(); + } +} diff --git a/app/src/main/java/javax/mail/search/FromStringTerm.java b/app/src/main/java/javax/mail/search/FromStringTerm.java new file mode 100644 index 0000000000..69ab926115 --- /dev/null +++ b/app/src/main/java/javax/mail/search/FromStringTerm.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; +import javax.mail.Address; + +/** + * This class implements string comparisons for the From Address + * header.

+ * + * Note that this class differs from the FromTerm class + * in that this class does comparisons on address strings rather than Address + * objects. The string comparisons are case-insensitive. + * + * @since JavaMail 1.1 + */ + +public final class FromStringTerm extends AddressStringTerm { + + private static final long serialVersionUID = 5801127523826772788L; + + /** + * Constructor. + * + * @param pattern the address pattern to be compared. + */ + public FromStringTerm(String pattern) { + super(pattern); + } + + /** + * Check whether the address string specified in the constructor is + * a substring of the From address of this Message. + * + * @param msg The comparison is applied to this Message's From + * address. + * @return true if the match succeeds, otherwise false. + */ + @Override + public boolean match(Message msg) { + Address[] from; + + try { + from = msg.getFrom(); + } catch (Exception e) { + return false; + } + + if (from == null) + return false; + + for (int i=0; i < from.length; i++) + if (super.match(from[i])) + return true; + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FromStringTerm)) + return false; + return super.equals(obj); + } +} diff --git a/app/src/main/java/javax/mail/search/FromTerm.java b/app/src/main/java/javax/mail/search/FromTerm.java new file mode 100644 index 0000000000..6320a9702d --- /dev/null +++ b/app/src/main/java/javax/mail/search/FromTerm.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; +import javax.mail.Address; + +/** + * This class implements comparisons for the From Address header. + * + * @author Bill Shannon + * @author John Mani + */ +public final class FromTerm extends AddressTerm { + + private static final long serialVersionUID = 5214730291502658665L; + + /** + * Constructor + * @param address The Address to be compared + */ + public FromTerm(Address address) { + super(address); + } + + /** + * The address comparator. + * + * @param msg The address comparison is applied to this Message + * @return true if the comparison succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + Address[] from; + + try { + from = msg.getFrom(); + } catch (Exception e) { + return false; + } + + if (from == null) + return false; + + for (int i=0; i < from.length; i++) + if (super.match(from[i])) + return true; + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FromTerm)) + return false; + return super.equals(obj); + } +} diff --git a/app/src/main/java/javax/mail/search/HeaderTerm.java b/app/src/main/java/javax/mail/search/HeaderTerm.java new file mode 100644 index 0000000000..72b82de0dc --- /dev/null +++ b/app/src/main/java/javax/mail/search/HeaderTerm.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import java.util.Locale; +import javax.mail.Message; + +/** + * This class implements comparisons for Message headers. + * The comparison is case-insensitive. + * + * @author Bill Shannon + * @author John Mani + */ +public final class HeaderTerm extends StringTerm { + /** + * The name of the header. + * + * @serial + */ + private String headerName; + + private static final long serialVersionUID = 8342514650333389122L; + + /** + * Constructor. + * + * @param headerName The name of the header + * @param pattern The pattern to search for + */ + public HeaderTerm(String headerName, String pattern) { + super(pattern); + this.headerName = headerName; + } + + /** + * Return the name of the header to compare with. + * + * @return the name of the header + */ + public String getHeaderName() { + return headerName; + } + + /** + * The header match method. + * + * @param msg The match is applied to this Message's header + * @return true if the match succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + String[] headers; + + try { + headers = msg.getHeader(headerName); + } catch (Exception e) { + return false; + } + + if (headers == null) + return false; + + for (int i=0; i < headers.length; i++) + if (super.match(headers[i])) + return true; + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof HeaderTerm)) + return false; + HeaderTerm ht = (HeaderTerm)obj; + // XXX - depends on header comparisons being case independent + return ht.headerName.equalsIgnoreCase(headerName) && super.equals(ht); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + // XXX - depends on header comparisons being case independent + return headerName.toLowerCase(Locale.ENGLISH).hashCode() + + super.hashCode(); + } +} diff --git a/app/src/main/java/javax/mail/search/IntegerComparisonTerm.java b/app/src/main/java/javax/mail/search/IntegerComparisonTerm.java new file mode 100644 index 0000000000..f89005126b --- /dev/null +++ b/app/src/main/java/javax/mail/search/IntegerComparisonTerm.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +/** + * This class implements comparisons for integers. + * + * @author Bill Shannon + * @author John Mani + */ +public abstract class IntegerComparisonTerm extends ComparisonTerm { + /** + * The number. + * + * @serial + */ + protected int number; + + private static final long serialVersionUID = -6963571240154302484L; + + protected IntegerComparisonTerm(int comparison, int number) { + this.comparison = comparison; + this.number = number; + } + + /** + * Return the number to compare with. + * + * @return the number + */ + public int getNumber() { + return number; + } + + /** + * Return the type of comparison. + * + * @return the comparison type + */ + public int getComparison() { + return comparison; + } + + protected boolean match(int i) { + switch (comparison) { + case LE: + return i <= number; + case LT: + return i < number; + case EQ: + return i == number; + case NE: + return i != number; + case GT: + return i > number; + case GE: + return i >= number; + default: + return false; + } + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof IntegerComparisonTerm)) + return false; + IntegerComparisonTerm ict = (IntegerComparisonTerm)obj; + return ict.number == this.number && super.equals(obj); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return number + super.hashCode(); + } +} diff --git a/app/src/main/java/javax/mail/search/MessageIDTerm.java b/app/src/main/java/javax/mail/search/MessageIDTerm.java new file mode 100644 index 0000000000..6762c0440a --- /dev/null +++ b/app/src/main/java/javax/mail/search/MessageIDTerm.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; + +/** + * This term models the RFC822 "MessageId" - a message-id for + * Internet messages that is supposed to be unique per message. + * Clients can use this term to search a folder for a message given + * its MessageId.

+ * + * The MessageId is represented as a String. + * + * @author Bill Shannon + * @author John Mani + */ +public final class MessageIDTerm extends StringTerm { + + private static final long serialVersionUID = -2121096296454691963L; + + /** + * Constructor. + * + * @param msgid the msgid to search for + */ + public MessageIDTerm(String msgid) { + // Note: comparison is case-insensitive + super(msgid); + } + + /** + * The match method. + * + * @param msg the match is applied to this Message's + * Message-ID header + * @return true if the match succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + String[] s; + + try { + s = msg.getHeader("Message-ID"); + } catch (Exception e) { + return false; + } + + if (s == null) + return false; + + for (int i=0; i < s.length; i++) + if (super.match(s[i])) + return true; + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof MessageIDTerm)) + return false; + return super.equals(obj); + } +} diff --git a/app/src/main/java/javax/mail/search/MessageNumberTerm.java b/app/src/main/java/javax/mail/search/MessageNumberTerm.java new file mode 100644 index 0000000000..3ec0f28735 --- /dev/null +++ b/app/src/main/java/javax/mail/search/MessageNumberTerm.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; + +/** + * This class implements comparisons for Message numbers. + * + * @author Bill Shannon + * @author John Mani + */ +public final class MessageNumberTerm extends IntegerComparisonTerm { + + private static final long serialVersionUID = -5379625829658623812L; + + /** + * Constructor. + * + * @param number the Message number + */ + public MessageNumberTerm(int number) { + super(EQ, number); + } + + /** + * The match method. + * + * @param msg the Message number is matched with this Message + * @return true if the match succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + int msgno; + + try { + msgno = msg.getMessageNumber(); + } catch (Exception e) { + return false; + } + + return super.match(msgno); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof MessageNumberTerm)) + return false; + return super.equals(obj); + } +} diff --git a/app/src/main/java/javax/mail/search/NotTerm.java b/app/src/main/java/javax/mail/search/NotTerm.java new file mode 100644 index 0000000000..b87c1516cb --- /dev/null +++ b/app/src/main/java/javax/mail/search/NotTerm.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; + +/** + * This class implements the logical NEGATION operator. + * + * @author Bill Shannon + * @author John Mani + */ +public final class NotTerm extends SearchTerm { + /** + * The search term to negate. + * + * @serial + */ + private SearchTerm term; + + private static final long serialVersionUID = 7152293214217310216L; + + public NotTerm(SearchTerm t) { + term = t; + } + + /** + * Return the term to negate. + * + * @return the Term + */ + public SearchTerm getTerm() { + return term; + } + + /* The NOT operation */ + @Override + public boolean match(Message msg) { + return !term.match(msg); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof NotTerm)) + return false; + NotTerm nt = (NotTerm)obj; + return nt.term.equals(this.term); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return term.hashCode() << 1; + } +} diff --git a/app/src/main/java/javax/mail/search/OrTerm.java b/app/src/main/java/javax/mail/search/OrTerm.java new file mode 100644 index 0000000000..3df58f46bc --- /dev/null +++ b/app/src/main/java/javax/mail/search/OrTerm.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; + +/** + * This class implements the logical OR operator on individual SearchTerms. + * + * @author Bill Shannon + * @author John Mani + */ +public final class OrTerm extends SearchTerm { + + /** + * The array of terms on which the OR operator should + * be applied. + * + * @serial + */ + private SearchTerm[] terms; + + private static final long serialVersionUID = 5380534067523646936L; + + /** + * Constructor that takes two operands. + * + * @param t1 first term + * @param t2 second term + */ + public OrTerm(SearchTerm t1, SearchTerm t2) { + terms = new SearchTerm[2]; + terms[0] = t1; + terms[1] = t2; + } + + /** + * Constructor that takes an array of SearchTerms. + * + * @param t array of search terms + */ + public OrTerm(SearchTerm[] t) { + terms = new SearchTerm[t.length]; + for (int i = 0; i < t.length; i++) + terms[i] = t[i]; + } + + /** + * Return the search terms. + * + * @return the search terms + */ + public SearchTerm[] getTerms() { + return terms.clone(); + } + + /** + * The OR operation.

+ * + * The terms specified in the constructor are applied to + * the given object and the OR operator is applied to their results. + * + * @param msg The specified SearchTerms are applied to this Message + * and the OR operator is applied to their results. + * @return true if the OR succeds, otherwise false + */ + + @Override + public boolean match(Message msg) { + for (int i=0; i < terms.length; i++) + if (terms[i].match(msg)) + return true; + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof OrTerm)) + return false; + OrTerm ot = (OrTerm)obj; + if (ot.terms.length != terms.length) + return false; + for (int i=0; i < terms.length; i++) + if (!terms[i].equals(ot.terms[i])) + return false; + return true; + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + int hash = 0; + for (int i=0; i < terms.length; i++) + hash += terms[i].hashCode(); + return hash; + } +} diff --git a/app/src/main/java/javax/mail/search/ReceivedDateTerm.java b/app/src/main/java/javax/mail/search/ReceivedDateTerm.java new file mode 100644 index 0000000000..b189d011ce --- /dev/null +++ b/app/src/main/java/javax/mail/search/ReceivedDateTerm.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import java.util.Date; +import javax.mail.Message; + +/** + * This class implements comparisons for the Message Received date + * + * @author Bill Shannon + * @author John Mani + */ +public final class ReceivedDateTerm extends DateTerm { + + private static final long serialVersionUID = -2756695246195503170L; + + /** + * Constructor. + * + * @param comparison the Comparison type + * @param date the date to be compared + */ + public ReceivedDateTerm(int comparison, Date date) { + super(comparison, date); + } + + /** + * The match method. + * + * @param msg the date comparator is applied to this Message's + * received date + * @return true if the comparison succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + Date d; + + try { + d = msg.getReceivedDate(); + } catch (Exception e) { + return false; + } + + if (d == null) + return false; + + return super.match(d); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ReceivedDateTerm)) + return false; + return super.equals(obj); + } +} diff --git a/app/src/main/java/javax/mail/search/RecipientStringTerm.java b/app/src/main/java/javax/mail/search/RecipientStringTerm.java new file mode 100644 index 0000000000..60466cd62f --- /dev/null +++ b/app/src/main/java/javax/mail/search/RecipientStringTerm.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; +import javax.mail.Address; + +/** + * This class implements string comparisons for the Recipient Address + * headers.

+ * + * Note that this class differs from the RecipientTerm class + * in that this class does comparisons on address strings rather than Address + * objects. The string comparisons are case-insensitive. + * + * @since JavaMail 1.1 + */ + +public final class RecipientStringTerm extends AddressStringTerm { + + /** + * The recipient type. + * + * @serial + */ + private Message.RecipientType type; + + private static final long serialVersionUID = -8293562089611618849L; + + /** + * Constructor. + * + * @param type the recipient type + * @param pattern the address pattern to be compared. + */ + public RecipientStringTerm(Message.RecipientType type, String pattern) { + super(pattern); + this.type = type; + } + + /** + * Return the type of recipient to match with. + * + * @return the recipient type + */ + public Message.RecipientType getRecipientType() { + return type; + } + + /** + * Check whether the address specified in the constructor is + * a substring of the recipient address of this Message. + * + * @param msg The comparison is applied to this Message's recipient + * address. + * @return true if the match succeeds, otherwise false. + */ + @Override + public boolean match(Message msg) { + Address[] recipients; + + try { + recipients = msg.getRecipients(type); + } catch (Exception e) { + return false; + } + + if (recipients == null) + return false; + + for (int i=0; i < recipients.length; i++) + if (super.match(recipients[i])) + return true; + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof RecipientStringTerm)) + return false; + RecipientStringTerm rst = (RecipientStringTerm)obj; + return rst.type.equals(this.type) && super.equals(obj); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return type.hashCode() + super.hashCode(); + } +} diff --git a/app/src/main/java/javax/mail/search/RecipientTerm.java b/app/src/main/java/javax/mail/search/RecipientTerm.java new file mode 100644 index 0000000000..7f2d149c73 --- /dev/null +++ b/app/src/main/java/javax/mail/search/RecipientTerm.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; +import javax.mail.Address; + +/** + * This class implements comparisons for the Recipient Address headers. + * + * @author Bill Shannon + * @author John Mani + */ +public final class RecipientTerm extends AddressTerm { + + /** + * The recipient type. + * + * @serial + */ + private Message.RecipientType type; + + private static final long serialVersionUID = 6548700653122680468L; + + /** + * Constructor. + * + * @param type the recipient type + * @param address the address to match for + */ + public RecipientTerm(Message.RecipientType type, Address address) { + super(address); + this.type = type; + } + + /** + * Return the type of recipient to match with. + * + * @return the recipient type + */ + public Message.RecipientType getRecipientType() { + return type; + } + + /** + * The match method. + * + * @param msg The address match is applied to this Message's recepient + * address + * @return true if the match succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + Address[] recipients; + + try { + recipients = msg.getRecipients(type); + } catch (Exception e) { + return false; + } + + if (recipients == null) + return false; + + for (int i=0; i < recipients.length; i++) + if (super.match(recipients[i])) + return true; + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof RecipientTerm)) + return false; + RecipientTerm rt = (RecipientTerm)obj; + return rt.type.equals(this.type) && super.equals(obj); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return type.hashCode() + super.hashCode(); + } +} diff --git a/app/src/main/java/javax/mail/search/SearchException.java b/app/src/main/java/javax/mail/search/SearchException.java new file mode 100644 index 0000000000..5c4d460fdf --- /dev/null +++ b/app/src/main/java/javax/mail/search/SearchException.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.MessagingException; + + +/** + * The exception thrown when a Search expression could not be handled. + * + * @author John Mani + */ + +public class SearchException extends MessagingException { + + private static final long serialVersionUID = -7092886778226268686L; + + /** + * Constructs a SearchException with no detail message. + */ + public SearchException() { + super(); + } + + /** + * Constructs a SearchException with the specified detail message. + * @param s the detail message + */ + public SearchException(String s) { + super(s); + } +} diff --git a/app/src/main/java/javax/mail/search/SearchTerm.java b/app/src/main/java/javax/mail/search/SearchTerm.java new file mode 100644 index 0000000000..5a890a2f1e --- /dev/null +++ b/app/src/main/java/javax/mail/search/SearchTerm.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import java.io.Serializable; + +import javax.mail.Message; + +/** + * Search criteria are expressed as a tree of search-terms, forming + * a parse-tree for the search expression.

+ * + * Search-terms are represented by this class. This is an abstract + * class; subclasses implement specific match methods.

+ * + * Search terms are serializable, which allows storing a search term + * between sessions. + * + * Warning: + * Serialized objects of this class may not be compatible with future + * Jakarta Mail API releases. The current serialization support is + * appropriate for short term storage.

+ * + * @author Bill Shannon + * @author John Mani + */ +public abstract class SearchTerm implements Serializable { + + private static final long serialVersionUID = -6652358452205992789L; + + /** + * This method applies a specific match criterion to the given + * message and returns the result. + * + * @param msg The match criterion is applied on this message + * @return true, it the match succeeds, false if the match fails + */ + + public abstract boolean match(Message msg); +} diff --git a/app/src/main/java/javax/mail/search/SentDateTerm.java b/app/src/main/java/javax/mail/search/SentDateTerm.java new file mode 100644 index 0000000000..2bd2fcb0cf --- /dev/null +++ b/app/src/main/java/javax/mail/search/SentDateTerm.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import java.util.Date; +import javax.mail.Message; + +/** + * This class implements comparisons for the Message SentDate. + * + * @author Bill Shannon + * @author John Mani + */ +public final class SentDateTerm extends DateTerm { + + private static final long serialVersionUID = 5647755030530907263L; + + /** + * Constructor. + * + * @param comparison the Comparison type + * @param date the date to be compared + */ + public SentDateTerm(int comparison, Date date) { + super(comparison, date); + } + + /** + * The match method. + * + * @param msg the date comparator is applied to this Message's + * sent date + * @return true if the comparison succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + Date d; + + try { + d = msg.getSentDate(); + } catch (Exception e) { + return false; + } + + if (d == null) + return false; + + return super.match(d); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SentDateTerm)) + return false; + return super.equals(obj); + } +} diff --git a/app/src/main/java/javax/mail/search/SizeTerm.java b/app/src/main/java/javax/mail/search/SizeTerm.java new file mode 100644 index 0000000000..a940176593 --- /dev/null +++ b/app/src/main/java/javax/mail/search/SizeTerm.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; + +/** + * This class implements comparisons for Message sizes. + * + * @author Bill Shannon + * @author John Mani + */ +public final class SizeTerm extends IntegerComparisonTerm { + + private static final long serialVersionUID = -2556219451005103709L; + + /** + * Constructor. + * + * @param comparison the Comparison type + * @param size the size + */ + public SizeTerm(int comparison, int size) { + super(comparison, size); + } + + /** + * The match method. + * + * @param msg the size comparator is applied to this Message's size + * @return true if the size is equal, otherwise false + */ + @Override + public boolean match(Message msg) { + int size; + + try { + size = msg.getSize(); + } catch (Exception e) { + return false; + } + + if (size == -1) + return false; + + return super.match(size); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SizeTerm)) + return false; + return super.equals(obj); + } +} diff --git a/app/src/main/java/javax/mail/search/StringTerm.java b/app/src/main/java/javax/mail/search/StringTerm.java new file mode 100644 index 0000000000..51edfae223 --- /dev/null +++ b/app/src/main/java/javax/mail/search/StringTerm.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +/** + * This class implements the match method for Strings. The current + * implementation provides only for substring matching. We + * could add comparisons (like strcmp ...). + * + * @author Bill Shannon + * @author John Mani + */ +public abstract class StringTerm extends SearchTerm { + /** + * The pattern. + * + * @serial + */ + protected String pattern; + + /** + * Ignore case when comparing? + * + * @serial + */ + protected boolean ignoreCase; + + private static final long serialVersionUID = 1274042129007696269L; + + /** + * Construct a StringTerm with the given pattern. + * Case will be ignored. + * + * @param pattern the pattern + */ + protected StringTerm(String pattern) { + this.pattern = pattern; + ignoreCase = true; + } + + /** + * Construct a StringTerm with the given pattern and ignoreCase flag. + * + * @param pattern the pattern + * @param ignoreCase should we ignore case? + */ + protected StringTerm(String pattern, boolean ignoreCase) { + this.pattern = pattern; + this.ignoreCase = ignoreCase; + } + + /** + * Return the string to match with. + * + * @return the string to match + */ + public String getPattern() { + return pattern; + } + + /** + * Return true if we should ignore case when matching. + * + * @return true if we should ignore case + */ + public boolean getIgnoreCase() { + return ignoreCase; + } + + protected boolean match(String s) { + int len = s.length() - pattern.length(); + for (int i=0; i <= len; i++) { + if (s.regionMatches(ignoreCase, i, + pattern, 0, pattern.length())) + return true; + } + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof StringTerm)) + return false; + StringTerm st = (StringTerm)obj; + if (ignoreCase) + return st.pattern.equalsIgnoreCase(this.pattern) && + st.ignoreCase == this.ignoreCase; + else + return st.pattern.equals(this.pattern) && + st.ignoreCase == this.ignoreCase; + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return ignoreCase ? pattern.hashCode() : ~pattern.hashCode(); + } +} diff --git a/app/src/main/java/javax/mail/search/SubjectTerm.java b/app/src/main/java/javax/mail/search/SubjectTerm.java new file mode 100644 index 0000000000..3326180d96 --- /dev/null +++ b/app/src/main/java/javax/mail/search/SubjectTerm.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.search; + +import javax.mail.Message; + +/** + * This class implements comparisons for the message Subject header. + * The comparison is case-insensitive. The pattern is a simple string + * that must appear as a substring in the Subject. + * + * @author Bill Shannon + * @author John Mani + */ +public final class SubjectTerm extends StringTerm { + + private static final long serialVersionUID = 7481568618055573432L; + + /** + * Constructor. + * + * @param pattern the pattern to search for + */ + public SubjectTerm(String pattern) { + // Note: comparison is case-insensitive + super(pattern); + } + + /** + * The match method. + * + * @param msg the pattern match is applied to this Message's + * subject header + * @return true if the pattern match succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + String subj; + + try { + subj = msg.getSubject(); + } catch (Exception e) { + return false; + } + + if (subj == null) + return false; + + return super.match(subj); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SubjectTerm)) + return false; + return super.equals(obj); + } +} diff --git a/app/src/main/java/javax/mail/search/package.html b/app/src/main/java/javax/mail/search/package.html new file mode 100644 index 0000000000..b11ddc2862 --- /dev/null +++ b/app/src/main/java/javax/mail/search/package.html @@ -0,0 +1,49 @@ + + + + + + +javax.mail.search package + + + +

+Message search terms for the Jakarta Mail API. +This package defines classes that can be used to construct a search +expression to search a folder for messages matching the expression; +see the {@link javax.mail.Folder#search search} method on +{@link javax.mail.Folder javax.mail.Folder}. +See {@link javax.mail.search.SearchTerm SearchTerm}. +

+

+Note that the exact search capabilities depend on the protocol, +provider, and server in use. For the POP3 protocol, all searching is +done on the client side using the Jakarta Mail classes. For IMAP, all +searching is done on the server side and is limited by the search +capabilities of the IMAP protocol and the IMAP server being used. +For example, IMAP date based searches have only day granularity. +

+

+In general, all of the string patterns supported by search terms are +just simple strings; no regular expressions are supported. +

+ + + diff --git a/app/src/main/java/javax/mail/util/ByteArrayDataSource.java b/app/src/main/java/javax/mail/util/ByteArrayDataSource.java new file mode 100644 index 0000000000..e4b9e5b145 --- /dev/null +++ b/app/src/main/java/javax/mail/util/ByteArrayDataSource.java @@ -0,0 +1,180 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.util; + +import java.io.*; +import javax.activation.*; +import javax.mail.internet.*; + +/** + * A DataSource backed by a byte array. The byte array may be + * passed in directly, or may be initialized from an InputStream + * or a String. + * + * @since JavaMail 1.4 + * @author John Mani + * @author Bill Shannon + * @author Max Spivak + */ +public class ByteArrayDataSource implements DataSource { + private byte[] data; // data + private int len = -1; + private String type; // content-type + private String name = ""; + + static class DSByteArrayOutputStream extends ByteArrayOutputStream { + public byte[] getBuf() { + return buf; + } + + public int getCount() { + return count; + } + } + + /** + * Create a ByteArrayDataSource with data from the + * specified InputStream and with the specified MIME type. + * The InputStream is read completely and the data is + * stored in a byte array. + * + * @param is the InputStream + * @param type the MIME type + * @exception IOException errors reading the stream + */ + public ByteArrayDataSource(InputStream is, String type) throws IOException { + DSByteArrayOutputStream os = new DSByteArrayOutputStream(); + byte[] buf = new byte[8192]; + int len; + while ((len = is.read(buf)) > 0) + os.write(buf, 0, len); + this.data = os.getBuf(); + this.len = os.getCount(); + + /* + * ByteArrayOutputStream doubles the size of the buffer every time + * it needs to expand, which can waste a lot of memory in the worst + * case with large buffers. Check how much is wasted here and if + * it's too much, copy the data into a new buffer and allow the + * old buffer to be garbage collected. + */ + if (this.data.length - this.len > 256*1024) { + this.data = os.toByteArray(); + this.len = this.data.length; // should be the same + } + this.type = type; + } + + /** + * Create a ByteArrayDataSource with data from the + * specified byte array and with the specified MIME type. + * + * @param data the data + * @param type the MIME type + */ + public ByteArrayDataSource(byte[] data, String type) { + this.data = data; + this.type = type; + } + + /** + * Create a ByteArrayDataSource with data from the + * specified String and with the specified MIME type. + * The MIME type should include a charset + * parameter specifying the charset to be used for the + * string. If the parameter is not included, the + * default charset is used. + * + * @param data the String + * @param type the MIME type + * @exception IOException errors reading the String + */ + public ByteArrayDataSource(String data, String type) throws IOException { + String charset = null; + try { + ContentType ct = new ContentType(type); + charset = ct.getParameter("charset"); + } catch (ParseException pex) { + // ignore parse error + } + charset = MimeUtility.javaCharset(charset); + if (charset == null) + charset = MimeUtility.getDefaultJavaCharset(); + // XXX - could convert to bytes on demand rather than copying here + this.data = data.getBytes(charset); + this.type = type; + } + + /** + * Return an InputStream for the data. + * Note that a new stream is returned each time + * this method is called. + * + * @return the InputStream + * @exception IOException if no data has been set + */ + @Override + public InputStream getInputStream() throws IOException { + if (data == null) + throw new IOException("no data"); + if (len < 0) + len = data.length; + return new SharedByteArrayInputStream(data, 0, len); + } + + /** + * Return an OutputStream for the data. + * Writing the data is not supported; an IOException + * is always thrown. + * + * @exception IOException always + */ + @Override + public OutputStream getOutputStream() throws IOException { + throw new IOException("cannot do this"); + } + + /** + * Get the MIME content type of the data. + * + * @return the MIME type + */ + @Override + public String getContentType() { + return type; + } + + /** + * Get the name of the data. + * By default, an empty string ("") is returned. + * + * @return the name of this data + */ + @Override + public String getName() { + return name; + } + + /** + * Set the name of the data. + * + * @param name the name of this data + */ + public void setName(String name) { + this.name = name; + } +} diff --git a/app/src/main/java/javax/mail/util/SharedByteArrayInputStream.java b/app/src/main/java/javax/mail/util/SharedByteArrayInputStream.java new file mode 100644 index 0000000000..0e13214df0 --- /dev/null +++ b/app/src/main/java/javax/mail/util/SharedByteArrayInputStream.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.util; + +import java.io.*; +import javax.mail.internet.SharedInputStream; + +/** + * A ByteArrayInputStream that implements the SharedInputStream interface, + * allowing the underlying byte array to be shared between multiple readers. + * + * @author Bill Shannon + * @since JavaMail 1.4 + */ + +public class SharedByteArrayInputStream extends ByteArrayInputStream + implements SharedInputStream { + /** + * Position within shared buffer that this stream starts at. + */ + protected int start = 0; + + /** + * Create a SharedByteArrayInputStream representing the entire + * byte array. + * + * @param buf the byte array + */ + public SharedByteArrayInputStream(byte[] buf) { + super(buf); + } + + /** + * Create a SharedByteArrayInputStream representing the part + * of the byte array from offset for length + * bytes. + * + * @param buf the byte array + * @param offset offset in byte array to first byte to include + * @param length number of bytes to include + */ + public SharedByteArrayInputStream(byte[] buf, int offset, int length) { + super(buf, offset, length); + start = offset; + } + + /** + * Return the current position in the InputStream, as an + * offset from the beginning of the InputStream. + * + * @return the current position + */ + @Override + public long getPosition() { + return pos - start; + } + + /** + * Return a new InputStream representing a subset of the data + * from this InputStream, starting at start (inclusive) + * up to end (exclusive). start must be + * non-negative. If end is -1, the new stream ends + * at the same place as this stream. The returned InputStream + * will also implement the SharedInputStream interface. + * + * @param start the starting position + * @param end the ending position + 1 + * @return the new stream + */ + @Override + public InputStream newStream(long start, long end) { + if (start < 0) + throw new IllegalArgumentException("start < 0"); + if (end == -1) + end = count - this.start; + return new SharedByteArrayInputStream(buf, + this.start + (int)start, (int)(end - start)); + } +} diff --git a/app/src/main/java/javax/mail/util/SharedFileInputStream.java b/app/src/main/java/javax/mail/util/SharedFileInputStream.java new file mode 100644 index 0000000000..31df0dab39 --- /dev/null +++ b/app/src/main/java/javax/mail/util/SharedFileInputStream.java @@ -0,0 +1,539 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package javax.mail.util; + +import java.io.*; +import javax.mail.internet.SharedInputStream; + +/** + * A SharedFileInputStream is a + * BufferedInputStream that buffers + * data from the file and supports the mark + * and reset methods. It also supports the + * newStream method that allows you to create + * other streams that represent subsets of the file. + * A RandomAccessFile object is used to + * access the file data.

+ * + * Note that when the SharedFileInputStream is closed, + * all streams created with the newStream + * method are also closed. This allows the creator of the + * SharedFileInputStream object to control access to the + * underlying file and ensure that it is closed when + * needed, to avoid leaking file descriptors. Note also + * that this behavior contradicts the requirements of + * SharedInputStream and may change in a future release. + * + * @author Bill Shannon + * @since JavaMail 1.4 + */ +public class SharedFileInputStream extends BufferedInputStream + implements SharedInputStream { + + private static int defaultBufferSize = 2048; + + /** + * The file containing the data. + * Shared by all related SharedFileInputStreams. + */ + protected RandomAccessFile in; + + /** + * The normal size of the read buffer. + */ + protected int bufsize; + + /** + * The file offset that corresponds to the first byte in + * the read buffer. + */ + protected long bufpos; + + /** + * The file offset of the start of data in this subset of the file. + */ + protected long start = 0; + + /** + * The amount of data in this subset of the file. + */ + protected long datalen; + + /** + * True if this is a top level stream created directly by "new". + * False if this is a derived stream created by newStream. + */ + private boolean master = true; + + /** + * A shared class that keeps track of the references + * to a particular file so it can be closed when the + * last reference is gone. + */ + static class SharedFile { + private int cnt; + private RandomAccessFile in; + + SharedFile(String file) throws IOException { + this.in = new RandomAccessFile(file, "r"); + } + + SharedFile(File file) throws IOException { + this.in = new RandomAccessFile(file, "r"); + } + + public synchronized RandomAccessFile open() { + cnt++; + return in; + } + + public synchronized void close() throws IOException { + if (cnt > 0 && --cnt <= 0) + in.close(); + } + + public synchronized void forceClose() throws IOException { + if (cnt > 0) { + // normal case, close exceptions propagated + cnt = 0; + in.close(); + } else { + // should already be closed, ignore exception + try { + in.close(); + } catch (IOException ioex) { } + } + } + + @Override + protected void finalize() throws Throwable { + try { + in.close(); + } finally { + super.finalize(); + } + } + } + + private SharedFile sf; + + /** + * Check to make sure that this stream has not been closed + */ + private void ensureOpen() throws IOException { + if (in == null) + throw new IOException("Stream closed"); + } + + /** + * Creates a SharedFileInputStream + * for the file. + * + * @param file the file + * @exception IOException for errors opening the file + */ + public SharedFileInputStream(File file) throws IOException { + this(file, defaultBufferSize); + } + + /** + * Creates a SharedFileInputStream + * for the named file + * + * @param file the file + * @exception IOException for errors opening the file + */ + public SharedFileInputStream(String file) throws IOException { + this(file, defaultBufferSize); + } + + /** + * Creates a SharedFileInputStream + * with the specified buffer size. + * + * @param file the file + * @param size the buffer size. + * @exception IOException for errors opening the file + * @exception IllegalArgumentException if size ≤ 0. + */ + public SharedFileInputStream(File file, int size) throws IOException { + super(null); // XXX - will it NPE? + if (size <= 0) + throw new IllegalArgumentException("Buffer size <= 0"); + init(new SharedFile(file), size); + } + + /** + * Creates a SharedFileInputStream + * with the specified buffer size. + * + * @param file the file + * @param size the buffer size. + * @exception IOException for errors opening the file + * @exception IllegalArgumentException if size ≤ 0. + */ + public SharedFileInputStream(String file, int size) throws IOException { + super(null); // XXX - will it NPE? + if (size <= 0) + throw new IllegalArgumentException("Buffer size <= 0"); + init(new SharedFile(file), size); + } + + private void init(SharedFile sf, int size) throws IOException { + this.sf = sf; + this.in = sf.open(); + this.start = 0; + this.datalen = in.length(); // XXX - file can't grow + this.bufsize = size; + buf = new byte[size]; + } + + /** + * Used internally by the newStream method. + */ + private SharedFileInputStream(SharedFile sf, long start, long len, + int bufsize) { + super(null); + this.master = false; + this.sf = sf; + this.in = sf.open(); + this.start = start; + this.bufpos = start; + this.datalen = len; + this.bufsize = bufsize; + buf = new byte[bufsize]; + } + + /** + * Fills the buffer with more data, taking into account + * shuffling and other tricks for dealing with marks. + * Assumes that it is being called by a synchronized method. + * This method also assumes that all data has already been read in, + * hence pos > count. + */ + private void fill() throws IOException { + if (markpos < 0) { + pos = 0; /* no mark: throw away the buffer */ + bufpos += count; + } else if (pos >= buf.length) /* no room left in buffer */ + if (markpos > 0) { /* can throw away early part of the buffer */ + int sz = pos - markpos; + System.arraycopy(buf, markpos, buf, 0, sz); + pos = sz; + bufpos += markpos; + markpos = 0; + } else if (buf.length >= marklimit) { + markpos = -1; /* buffer got too big, invalidate mark */ + pos = 0; /* drop buffer contents */ + bufpos += count; + } else { /* grow buffer */ + int nsz = pos * 2; + if (nsz > marklimit) + nsz = marklimit; + byte nbuf[] = new byte[nsz]; + System.arraycopy(buf, 0, nbuf, 0, pos); + buf = nbuf; + } + count = pos; + // limit to datalen + int len = buf.length - pos; + if (bufpos - start + pos + len > datalen) + len = (int)(datalen - (bufpos - start + pos)); + synchronized (in) { + in.seek(bufpos + pos); + int n = in.read(buf, pos, len); + if (n > 0) + count = n + pos; + } + } + + /** + * See the general contract of the read + * method of InputStream. + * + * @return the next byte of data, or -1 if the end of the + * stream is reached. + * @exception IOException if an I/O error occurs. + */ + @Override + public synchronized int read() throws IOException { + ensureOpen(); + if (pos >= count) { + fill(); + if (pos >= count) + return -1; + } + return buf[pos++] & 0xff; + } + + /** + * Read characters into a portion of an array, reading from the underlying + * stream at most once if necessary. + */ + private int read1(byte[] b, int off, int len) throws IOException { + int avail = count - pos; + if (avail <= 0) { + if (false) { + /* If the requested length is at least as large as the buffer, and + if there is no mark/reset activity, do not bother to copy the + bytes into the local buffer. In this way buffered streams will + cascade harmlessly. */ + if (len >= buf.length && markpos < 0) { + // XXX - seek, update bufpos - how? + return in.read(b, off, len); + } + } + fill(); + avail = count - pos; + if (avail <= 0) return -1; + } + int cnt = (avail < len) ? avail : len; + System.arraycopy(buf, pos, b, off, cnt); + pos += cnt; + return cnt; + } + + /** + * Reads bytes from this stream into the specified byte array, + * starting at the given offset. + * + *

This method implements the general contract of the corresponding + * {@link java.io.InputStream#read(byte[], int, int) read} + * method of the {@link java.io.InputStream} class. + * + * @param b destination buffer. + * @param off offset at which to start storing bytes. + * @param len maximum number of bytes to read. + * @return the number of bytes read, or -1 if the end of + * the stream has been reached. + * @exception IOException if an I/O error occurs. + */ + @Override + public synchronized int read(byte b[], int off, int len) + throws IOException + { + ensureOpen(); + if ((off | len | (off + len) | (b.length - (off + len))) < 0) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return 0; + } + + int n = read1(b, off, len); + if (n <= 0) return n; + while ((n < len) /* && (in.available() > 0) */) { + int n1 = read1(b, off + n, len - n); + if (n1 <= 0) break; + n += n1; + } + return n; + } + + /** + * See the general contract of the skip + * method of InputStream. + * + * @param n the number of bytes to be skipped. + * @return the actual number of bytes skipped. + * @exception IOException if an I/O error occurs. + */ + @Override + public synchronized long skip(long n) throws IOException { + ensureOpen(); + if (n <= 0) { + return 0; + } + long avail = count - pos; + + if (avail <= 0) { + // If no mark position set then don't keep in buffer + /* + if (markpos <0) + return in.skip(n); + */ + + // Fill in buffer to save bytes for reset + fill(); + avail = count - pos; + if (avail <= 0) + return 0; + } + + long skipped = (avail < n) ? avail : n; + pos += skipped; + return skipped; + } + + /** + * Returns the number of bytes that can be read from this input + * stream without blocking. + * + * @return the number of bytes that can be read from this input + * stream without blocking. + * @exception IOException if an I/O error occurs. + */ + @Override + public synchronized int available() throws IOException { + ensureOpen(); + return (count - pos) + in_available(); + } + + private int in_available() throws IOException { + // XXX - overflow + return (int)((start + datalen) - (bufpos + count)); + } + + /** + * See the general contract of the mark + * method of InputStream. + * + * @param readlimit the maximum limit of bytes that can be read before + * the mark position becomes invalid. + * @see #reset() + */ + @Override + public synchronized void mark(int readlimit) { + marklimit = readlimit; + markpos = pos; + } + + /** + * See the general contract of the reset + * method of InputStream. + *

+ * If markpos is -1 + * (no mark has been set or the mark has been + * invalidated), an IOException + * is thrown. Otherwise, pos is + * set equal to markpos. + * + * @exception IOException if this stream has not been marked or + * if the mark has been invalidated. + * @see #mark(int) + */ + @Override + public synchronized void reset() throws IOException { + ensureOpen(); + if (markpos < 0) + throw new IOException("Resetting to invalid mark"); + pos = markpos; + } + + /** + * Tests if this input stream supports the mark + * and reset methods. The markSupported + * method of SharedFileInputStream returns + * true. + * + * @return a boolean indicating if this stream type supports + * the mark and reset methods. + * @see java.io.InputStream#mark(int) + * @see java.io.InputStream#reset() + */ + @Override + public boolean markSupported() { + return true; + } + + /** + * Closes this input stream and releases any system resources + * associated with the stream. + * + * @exception IOException if an I/O error occurs. + */ + @Override + public void close() throws IOException { + if (in == null) + return; + try { + if (master) + sf.forceClose(); + else + sf.close(); + } finally { + sf = null; + in = null; + buf = null; + } + } + + /** + * Return the current position in the InputStream, as an + * offset from the beginning of the InputStream. + * + * @return the current position + */ + @Override + public long getPosition() { +//System.out.println("getPosition: start " + start + " pos " + pos +// + " bufpos " + bufpos + " = " + (bufpos + pos - start)); + if (in == null) + throw new RuntimeException("Stream closed"); + return bufpos + pos - start; + } + + /** + * Return a new InputStream representing a subset of the data + * from this InputStream, starting at start (inclusive) + * up to end (exclusive). start must be + * non-negative. If end is -1, the new stream ends + * at the same place as this stream. The returned InputStream + * will also implement the SharedInputStream interface. + * + * @param start the starting position + * @param end the ending position + 1 + * @return the new stream + */ + @Override + public synchronized InputStream newStream(long start, long end) { + if (in == null) + throw new RuntimeException("Stream closed"); + if (start < 0) + throw new IllegalArgumentException("start < 0"); + if (end == -1) + end = datalen; + return new SharedFileInputStream(sf, + this.start + start, end - start, bufsize); + } + + // for testing... + /* + public static void main(String[] argv) throws Exception { + SharedFileInputStream is = new SharedFileInputStream(argv[0]); + java.util.Random r = new java.util.Random(); + int b; + while ((b = is.read()) >= 0) { + System.out.write(b); + if (r.nextDouble() < 0.3) { + InputStream is2 = is.newStream(is.getPosition(), -1); + int b2; + while ((b2 = is2.read()) >= 0) + ; + } + } + } + */ + + /** + * Force this stream to close. + */ + @Override + protected void finalize() throws Throwable { + super.finalize(); + close(); + } +} diff --git a/app/src/main/java/javax/mail/util/package.html b/app/src/main/java/javax/mail/util/package.html new file mode 100644 index 0000000000..5351853a49 --- /dev/null +++ b/app/src/main/java/javax/mail/util/package.html @@ -0,0 +1,34 @@ + + + + + + +javax.mail.util package + + + +

+Jakarta Mail API utility classes. +This package specifies utility classes that are useful with +other Jakarta Mail APIs. +

+ + + diff --git a/app/src/main/resources/META-INF/gfprobe-provider.xml b/app/src/main/resources/META-INF/gfprobe-provider.xml new file mode 100644 index 0000000000..ec1ff35730 --- /dev/null +++ b/app/src/main/resources/META-INF/gfprobe-provider.xml @@ -0,0 +1,69 @@ + + + + + + + sendMessageStart + + + + + sendMessageEnd + + + + + + commandStart + + + + + commandEnd + + + + + + simpleCommandStart + + + + + simpleCommandEnd + + + + multilineCommandStart + + + + + multilineCommandEnd + + + + diff --git a/app/src/main/resources/META-INF/hk2-locator/default b/app/src/main/resources/META-INF/hk2-locator/default new file mode 100644 index 0000000000..226ff1d9cf --- /dev/null +++ b/app/src/main/resources/META-INF/hk2-locator/default @@ -0,0 +1,9 @@ +# +# This metadata allows com.sun.mail.util.logging.MailHandler to be +# configured in the logging.properties file and used in GlassFish. +# This file was created by hand to avoid a compile time dependency +# on the HK2 annotations. +# +[com.sun.mail.util.logging.MailHandler] +contract={java.util.logging.Handler} +scope=javax.inject.Singleton diff --git a/app/src/main/resources/META-INF/javamail.charset.map b/app/src/main/resources/META-INF/javamail.charset.map new file mode 100644 index 0000000000..cc58d4f986 --- /dev/null +++ b/app/src/main/resources/META-INF/javamail.charset.map @@ -0,0 +1,77 @@ +### JDK-to-MIME charset mapping table #### +### This should be the first mapping table ### + +8859_1 ISO-8859-1 +iso8859_1 ISO-8859-1 +ISO8859-1 ISO-8859-1 + +8859_2 ISO-8859-2 +iso8859_2 ISO-8859-2 +ISO8859-2 ISO-8859-2 + +8859_3 ISO-8859-3 +iso8859_3 ISO-8859-3 +ISO8859-3 ISO-8859-3 + +8859_4 ISO-8859-4 +iso8859_4 ISO-8859-4 +ISO8859-4 ISO-8859-4 + +8859_5 ISO-8859-5 +iso8859_5 ISO-8859-5 +ISO8859-5 ISO-8859-5 + +8859_6 ISO-8859-6 +iso8859_6 ISO-8859-6 +ISO8859-6 ISO-8859-6 + +8859_7 ISO-8859-7 +iso8859_7 ISO-8859-7 +ISO8859-7 ISO-8859-7 + +8859_8 ISO-8859-8 +iso8859_8 ISO-8859-8 +ISO8859-8 ISO-8859-8 + +8859_9 ISO-8859-9 +iso8859_9 ISO-8859-9 +ISO8859-9 ISO-8859-9 + +SJIS Shift_JIS +JIS ISO-2022-JP +ISO2022JP ISO-2022-JP +EUC_JP euc-jp +KOI8_R koi8-r +EUC_CN euc-cn +EUC_TW euc-tw +EUC_KR euc-kr + +--DIVIDER: this line *must* start with "--" and end with "--" -- + +#### XXX-to-JDK charset mapping table #### + +iso-2022-cn ISO2022CN +iso-2022-kr ISO2022KR +utf-8 UTF8 +utf8 UTF8 +ja_jp.iso2022-7 ISO2022JP +ja_jp.eucjp EUCJIS + +# these two are not needed in 1.1.6. (since EUC_KR exists +# and KSC5601 will map to the correct converter) +euc-kr KSC5601 +euckr KSC5601 + +# in JDK 1.1.6 we will no longer need the "us-ascii" convert +us-ascii ISO-8859-1 +x-us-ascii ISO-8859-1 + +# Chinese charsets are a mess and widely misrepresented. +# gb18030 is a superset of gbk, which is a supserset of cp936/ms936, +# which is a superset of gb2312. +# https://bugzilla.gnome.org/show_bug.cgi?id=446783 +# map all of these to gb18030. +gb2312 GB18030 +cp936 GB18030 +ms936 GB18030 +gbk GB18030 diff --git a/app/src/main/resources/META-INF/javamail.default.address.map b/app/src/main/resources/META-INF/javamail.default.address.map new file mode 100644 index 0000000000..4ab5572dfa --- /dev/null +++ b/app/src/main/resources/META-INF/javamail.default.address.map @@ -0,0 +1 @@ +rfc822=smtp diff --git a/app/src/main/resources/META-INF/javamail.default.providers b/app/src/main/resources/META-INF/javamail.default.providers new file mode 100644 index 0000000000..e8461a79df --- /dev/null +++ b/app/src/main/resources/META-INF/javamail.default.providers @@ -0,0 +1,9 @@ +# Jakarta Mail IMAP provider Oracle +protocol=imap; type=store; class=com.sun.mail.imap.IMAPStore; vendor=Oracle; +protocol=imaps; type=store; class=com.sun.mail.imap.IMAPSSLStore; vendor=Oracle; +# Jakarta Mail SMTP provider Oracle +protocol=smtp; type=transport; class=com.sun.mail.smtp.SMTPTransport; vendor=Oracle; +protocol=smtps; type=transport; class=com.sun.mail.smtp.SMTPSSLTransport; vendor=Oracle; +# Jakarta Mail POP3 provider Oracle +protocol=pop3; type=store; class=com.sun.mail.pop3.POP3Store; vendor=Oracle; +protocol=pop3s; type=store; class=com.sun.mail.pop3.POP3SSLStore; vendor=Oracle; diff --git a/app/src/main/resources/META-INF/mailcap b/app/src/main/resources/META-INF/mailcap new file mode 100644 index 0000000000..9a89634d3c --- /dev/null +++ b/app/src/main/resources/META-INF/mailcap @@ -0,0 +1,16 @@ +# +# +# Default mailcap file for the Jakarta Mail System. +# +# Jakarta Mail content-handlers: +# +text/plain;; x-java-content-handler=com.sun.mail.handlers.text_plain +text/html;; x-java-content-handler=com.sun.mail.handlers.text_html +text/xml;; x-java-content-handler=com.sun.mail.handlers.text_xml +multipart/*;; x-java-content-handler=com.sun.mail.handlers.multipart_mixed; x-java-fallback-entry=true +message/rfc822;; x-java-content-handler=com.sun.mail.handlers.message_rfc822 +# +# can't support image types because java.awt.Toolkit doesn't work on servers +# +#image/gif;; x-java-content-handler=com.sun.mail.handlers.image_gif +#image/jpeg;; x-java-content-handler=com.sun.mail.handlers.image_jpeg diff --git a/app/src/main/resources/META-INF/maven/com.sun.mail/jakarta.mail/pom.properties b/app/src/main/resources/META-INF/maven/com.sun.mail/jakarta.mail/pom.properties new file mode 100644 index 0000000000..7a4e63fd68 --- /dev/null +++ b/app/src/main/resources/META-INF/maven/com.sun.mail/jakarta.mail/pom.properties @@ -0,0 +1,3 @@ +version=1.6.5-SNAPSHOT +groupId=com.sun.mail +artifactId=jakarta.mail diff --git a/app/src/main/resources/META-INF/maven/com.sun.mail/jakarta.mail/pom.xml b/app/src/main/resources/META-INF/maven/com.sun.mail/jakarta.mail/pom.xml new file mode 100644 index 0000000000..45bd24270f --- /dev/null +++ b/app/src/main/resources/META-INF/maven/com.sun.mail/jakarta.mail/pom.xml @@ -0,0 +1,221 @@ + + + + + + com.sun.mail + all + 1.6.5-SNAPSHOT + + 4.0.0 + com.sun.mail + jakarta.mail + jar + Jakarta Mail API + + + + jakarta.mail + + + jakarta.mail + + + Jakarta Mail API Design Specification + + + javax.mail + + + javax.mail.*; version=${mail.spec.version}, + com.sun.mail.imap; version=${mail.osgiversion}, + com.sun.mail.imap.protocol; version=${mail.osgiversion}, + com.sun.mail.iap; version=${mail.osgiversion}, + com.sun.mail.pop3; version=${mail.osgiversion}, + com.sun.mail.smtp; version=${mail.osgiversion}, + com.sun.mail.util; version=${mail.osgiversion}, + com.sun.mail.util.logging; version=${mail.osgiversion}, + com.sun.mail.handlers; version=${mail.osgiversion} + + + META-INF/gfprobe-provider.xml + + + false + + + ${project.basedir}/exclude.xml + + + + + + + 1.7 + + 1.7 + + + + + maven-compiler-plugin + + + default-compile + + + + javax/mail/MailSessionDefinition.java + + + javax/mail/MailSessionDefinitions.java + + + module-info.java + + + + + + + + + + + + + 11 + + 11 + + + + + maven-compiler-plugin + + + + -Xlint + -Xlint:-options + -Xlint:-path + + + + + + + + + + + + + src/main/resources + true + + + + + + maven-compiler-plugin + + 1.8 + 1.8 + + -Xlint + -Xlint:-options + -Xlint:-path + -Werror + + true + true + + + module-info.java + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + **/*Test.java + **/*TestSuite.java + + + + + + + maven-jar-plugin + + + + + ${mail.moduleName} + + + + + + + + + + + com.sun.activation + jakarta.activation + + + junit + junit + 4.12 + test + true + + + diff --git a/app/src/main/resources/META-INF/services/javax.mail.Provider b/app/src/main/resources/META-INF/services/javax.mail.Provider new file mode 100644 index 0000000000..4688e5aace --- /dev/null +++ b/app/src/main/resources/META-INF/services/javax.mail.Provider @@ -0,0 +1,6 @@ +com.sun.mail.imap.IMAPProvider +com.sun.mail.imap.IMAPSSLProvider +com.sun.mail.smtp.SMTPProvider +com.sun.mail.smtp.SMTPSSLProvider +com.sun.mail.pop3.POP3Provider +com.sun.mail.pop3.POP3SSLProvider