brett 2004/06/27 00:57:49 Modified: src/java/org/apache/maven Tag: MAVEN-1_0-BRANCH MavenConstants.java . Tag: MAVEN-1_0-BRANCH build-bootstrap.xml project.xml src/java/org/apache/maven/util Tag: MAVEN-1_0-BRANCH HttpUtils.java src/java/org/apache/maven/verifier Tag: MAVEN-1_0-BRANCH DependencyVerifier.java src/bootstrap/org/apache/maven Tag: MAVEN-1_0-BRANCH BootstrapTask.java xdocs Tag: MAVEN-1_0-BRANCH changes.xml xdocs/reference Tag: MAVEN-1_0-BRANCH user-guide.xml Log: PR: MAVEN-1332 reworked HttpUtils so httpclient is used and NTLM proxy is supported Revision Changes Path No revision No revision 1.30.4.7 +5 -1 maven/src/java/org/apache/maven/MavenConstants.java Index: MavenConstants.java =================================================================== RCS file: /home/cvs/maven/src/java/org/apache/maven/MavenConstants.java,v retrieving revision 1.30.4.6 retrieving revision 1.30.4.7 diff -u -r1.30.4.6 -r1.30.4.7 --- MavenConstants.java 10 Apr 2004 00:57:34 -0000 1.30.4.6 +++ MavenConstants.java 27 Jun 2004 07:57:48 -0000 1.30.4.7 @@ -71,6 +71,10 @@ /** Proxy password tag. */ public static final String PROXY_PASSWORD = "maven.proxy.password"; + /** Proxy loginHost tag. */ + public static final String PROXY_LOGINHOST = "maven.proxy.ntlm.host"; + /** Proxy loginDomain tag. */ + public static final String PROXY_LOGINDOMAIN = "maven.proxy.ntlm.domain"; /** Snapshot JAR signifier tag. */ public static final String SNAPSHOT_SIGNIFIER = "SNAPSHOT"; No revision No revision 1.212.2.9 +9 -0 maven/build-bootstrap.xml Index: build-bootstrap.xml =================================================================== RCS file: /home/cvs/maven/build-bootstrap.xml,v retrieving revision 1.212.2.8 retrieving revision 1.212.2.9 diff -u -r1.212.2.8 -r1.212.2.9 --- build-bootstrap.xml 25 Jun 2004 16:53:17 -0000 1.212.2.8 +++ build-bootstrap.xml 27 Jun 2004 07:57:48 -0000 1.212.2.9 @@ -342,6 +342,11 @@ <delete dir="${maven.bootstrap.dir}"/> <mkdir dir="${maven.bootstrap.classes}"/> + <property name="cl-jar" value="commons-logging/jars/commons-logging-1.0.3.jar" /> + <property name="ch-jar" value="commons-httpclient/jars/commons-httpclient-2.0.jar" /> + <get ignoreerrors="true" usetimestamp="true" dest="${maven.repo.local}/${cl-jar}" src="${maven.get.jars.baseUrl}" /> + <get ignoreerrors="true" usetimestamp="true" dest="${maven.repo.local}/${ch-jar}" src="${maven.get.jars.baseUrl}" /> + <javac destdir="${maven.bootstrap.classes}" debug="on" @@ -351,6 +356,10 @@ <include name="bootstrap/**"/> <include name="java/**/HttpUtils*"/> <include name="java/**/Base64*"/> + <classpath> + <pathelement location="${maven.repo.local}/${cl-jar}" /> + <pathelement location="${maven.repo.local}/${ch-jar}" /> + </classpath> </javac> </target> 1.317.4.27 +5 -0 maven/project.xml Index: project.xml =================================================================== RCS file: /home/cvs/maven/project.xml,v retrieving revision 1.317.4.26 retrieving revision 1.317.4.27 diff -u -r1.317.4.26 -r1.317.4.27 --- project.xml 29 May 2004 04:46:28 -0000 1.317.4.26 +++ project.xml 27 Jun 2004 07:57:48 -0000 1.317.4.27 @@ -646,6 +646,11 @@ <url>http://jakarta.apache.org/commons/logging.html</url> </dependency> <dependency> + <groupId>commons-httpclient</groupId> + <artifactId>commons-httpclient</artifactId> + <version>2.0</version> + </dependency> + <dependency> <id>werkz</id> <version>20040426.222000</version> <url>http://werkz.codehaus.org/</url> No revision No revision 1.28.4.10 +288 -91 maven/src/java/org/apache/maven/util/HttpUtils.java Index: HttpUtils.java =================================================================== RCS file: /home/cvs/maven/src/java/org/apache/maven/util/HttpUtils.java,v retrieving revision 1.28.4.9 retrieving revision 1.28.4.10 diff -u -r1.28.4.9 -r1.28.4.10 --- HttpUtils.java 12 Jun 2004 01:21:24 -0000 1.28.4.9 +++ HttpUtils.java 27 Jun 2004 07:57:48 -0000 1.28.4.10 @@ -22,11 +22,29 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.net.Authenticator; import java.net.HttpURLConnection; +import java.net.MalformedURLException; import java.net.PasswordAuthentication; import java.net.URL; import java.net.URLConnection; +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.commons.httpclient.Credentials; +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HostConfiguration; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpRecoverableException; +import org.apache.commons.httpclient.NTCredentials; +import org.apache.commons.httpclient.UsernamePasswordCredentials; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.commons.httpclient.protocol.Protocol; +import org.apache.commons.httpclient.util.DateParser; +import org.apache.commons.httpclient.util.DateParseException; /** * Http utils for retrieving files. @@ -41,6 +59,8 @@ */ public class HttpUtils { + private static final Log LOG = LogFactory.getLog(HttpUtils.class); + /** * Use a proxy to bypass the firewall with or without authentication * @@ -96,7 +116,6 @@ * or null. * @param useChecksum Flag to indicate the use of the checksum for the retrieved * artifact if it is available. - * @throws IOException If an I/O exception occurs. */ public static void getFile( String url, File destinationFile, @@ -109,6 +128,44 @@ boolean useChecksum ) throws IOException { + getFile( url, destinationFile, ignoreErrors, useTimestamp, proxyHost, proxyPort, proxyUserName, proxyPassword, null, null, useChecksum ); + } + + /** + * Retrieve a remote file. Throws an Exception on errors unless the + * ifnoreErrors flag is set to True + * + * @param url the of the file to retrieve + * @param destinationFile where to store it + * @param ignoreErrors whether to ignore errors during I/O or throw an + * exception when they happen + * @param useTimestamp whether to check the modified timestamp on the + * <code>destinationFile</code> against the remote <code>source</code> + * @param proxyHost Proxy Host (if proxy is required), or null + * @param proxyPort Proxy Port (if proxy is required), or null + * @param proxyUserName Proxy Username (if authentification is required), + * or null. + * @param proxyPassword Proxy Password (if authentification is required), + * or null. + * @param useChecksum Flag to indicate the use of the checksum for the retrieved + * artifact if it is available. + * @param loginHost The host the authentication request is originating from. + * Essentially, the computer name for this machine. + * @param loginDomain the domain to authenticate within. + */ + public static void getFile( String url, + File destinationFile, + boolean ignoreErrors, + boolean useTimestamp, + String proxyHost, + String proxyPort, + String proxyUserName, + String proxyPassword, + String loginHost, + String loginDomain, + boolean useChecksum ) + throws IOException + { // Get the requested file. getFile( url, destinationFile, @@ -117,7 +174,7 @@ proxyHost, proxyPort, proxyUserName, - proxyPassword ); + proxyPassword, loginHost, loginDomain ); // Get the checksum if requested. if ( useChecksum ) @@ -133,7 +190,7 @@ proxyHost, proxyPort, proxyUserName, - proxyPassword ); + proxyPassword, loginHost, loginDomain ); } catch ( Exception e ) { @@ -159,7 +216,6 @@ * or null * @param proxyPassword Proxy Password (if authentification is required), * or null - * @throws IOException If an I/O exception occurs. */ public static void getFile( String url, File destinationFile, @@ -171,6 +227,41 @@ String proxyPassword ) throws IOException { + getFile( url, destinationFile, ignoreErrors, useTimestamp, proxyHost, proxyPort, proxyUserName, proxyPassword, null, null ); + } + + /** + * Retrieve a remote file. Throws an Exception on errors unless the + * ifnoreErrors flag is set to True + * + * @param url the of the file to retrieve + * @param destinationFile where to store it + * @param ignoreErrors whether to ignore errors during I/O or throw an + * exception when they happen + * @param useTimestamp whether to check the modified timestamp on the + * <code>destinationFile</code> against the remote <code>source</code> + * @param proxyHost Proxy Host (if proxy is required), or null + * @param proxyPort Proxy Port (if proxy is required), or null + * @param proxyUserName Proxy Username (if authentification is required), + * or null + * @param proxyPassword Proxy Password (if authentification is required), + * or null + * @param loginHost The host the authentication request is originating from. + * Essentially, the computer name for this machine. + * @param loginDomain the domain to authenticate within. + */ + public static void getFile( String url, + File destinationFile, + boolean ignoreErrors, + boolean useTimestamp, + String proxyHost, + String proxyPort, + String proxyUserName, + String proxyPassword, + String loginHost, + String loginDomain ) + throws IOException + { //set the timestamp to the file date. long timestamp = -1; if ( useTimestamp && destinationFile.exists() ) @@ -186,7 +277,9 @@ proxyHost, proxyPort, proxyUserName, - proxyPassword ); + proxyPassword, + loginHost, + loginDomain ); } catch ( IOException ex ) { @@ -211,7 +304,10 @@ * or null * @param proxyPassword Proxy Password (if authentification is required), * or null - * @throws IOException If an I/O exception occurs. + * @param loginHost The host the authentication request is originating from. + * Essentially, the computer name for this machine. + * @param loginDomain the domain to authenticate within. + * @exception IOException If an I/O exception occurs. */ public static void getFile( String url, File destinationFile, @@ -219,121 +315,221 @@ String proxyHost, String proxyPort, String proxyUserName, - String proxyPassword ) + String proxyPassword, + String loginHost, + String loginDomain ) throws IOException { + boolean silent = url.endsWith(".md5"); + String[] s = parseUrl( url ); String username = s[0]; String password = s[1]; String parsedUrl = s[2]; URL source = new URL( parsedUrl ); + if (source.getProtocol().equals("http") || source.getProtocol().equals("https")) { + Credentials creds = null; + if (!empty(loginHost) || !empty(loginDomain)) { + creds = new NTCredentials(proxyUserName, proxyPassword, loginHost, loginDomain); + } + else if (!empty(proxyUserName) || !empty(proxyPassword)) { + creds = new UsernamePasswordCredentials(proxyUserName, proxyPassword); + } + + HttpClient client = new HttpClient(); + HostConfiguration hc = new HostConfiguration(); + hc.setHost(source.getHost(), source.getPort(), source.getProtocol()); + + if (!empty(proxyHost) || !empty(proxyPort)) { + try { + hc.setProxy(proxyHost, Integer.parseInt(proxyPort)); + } catch (NumberFormatException e) { + LOG.error("Unable to parse proxy port '"+proxyPort+"', ignoring proxy settings"); + } + } + client.setHostConfiguration(hc); + if (creds != null) { + client.getState().setProxyCredentials(null, null, creds); + } + + // Basic authentication + if ( username != null || password != null ) + { + creds = new UsernamePasswordCredentials(username, password); + client.getState().setCredentials(null, null, creds); + } + + int statusCode = -1; + InputStream is = null; + GetMethod get = new GetMethod(source.getPath()); + try { + if ( timestamp >= 0 ) + { + SimpleDateFormat fmt = new SimpleDateFormat("EEE, dd-MMM-yy HH:mm:ss zzz"); + get.addRequestHeader(new Header("If-Modified-Since", fmt.format(new Date(timestamp)))); + } - //set proxy connection - useProxyUser( proxyHost, proxyPort, proxyUserName, proxyPassword ); - - //set up the URL connection - URLConnection connection = source.openConnection(); - - //modify the headers - if ( timestamp >= 0 ) - { - connection.setIfModifiedSince( timestamp ); - } - // prepare Java 1.1 style credentials - if ( username != null || password != null ) - { - String up = username + ":" + password; - String encoding = Base64.encode(up.getBytes(), false); - connection.setRequestProperty( "Authorization", "Basic " + encoding ); - } - - //connect to the remote site (may take some time) - connection.connect(); - //next test for a 304 result (HTTP only) - if ( connection instanceof HttpURLConnection ) - { - HttpURLConnection httpConnection = (HttpURLConnection) connection; - // although HTTPUrlConnection javadocs says FileNotFoundException should be - // thrown on a 404 error, that certainly does not appear to be the case, so - // test for 404 ourselves, and throw FileNotFoundException as needed - if ( httpConnection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) - { - throw new FileNotFoundException(url.toString() + " (HTTP Error: " - + httpConnection.getResponseCode() + " " + httpConnection.getResponseMessage() + ")"); + // We will retry up to 3 times. + for (int i = 0; i < 3; i++) + { + try + { + statusCode = client.executeMethod(get); + if (statusCode != -1) { + break; + } + } + catch (HttpRecoverableException e) + { + if (i < 2) { + throw e; + } + LOG.warn( "A recoverable exception occurred." + e.getMessage()); + } + catch (IOException e) + { + throw e; + } + if (i < 2) { + LOG.warn("retrying " + (i + 1) + " of 3"); + } + } + + boolean use = statusCode < 300 && statusCode != HttpURLConnection.HTTP_NOT_MODIFIED; + + // Must read content regardless + is = get.getResponseBodyAsStream(); + if (is == null) { + if (!silent) LOG.info("Not modified"); + return; + } + + int projected = 0; + Header header = get.getResponseHeader("Content-Length"); + if (header != null) { + projected = Integer.valueOf(header.getValue()).intValue()/1024; + } + + long remoteTimestamp = 0; + header = get.getResponseHeader("Last-Modified"); + if (header != null) { + try { + remoteTimestamp = DateParser.parseDate(header.getValue()).getTime(); + } + catch (DateParseException e) { + LOG.warn("Unable to parse last modified header", e ); + } + } + else { + if (!silent) LOG.warn("warning: last-modified not specified"); + } + process( use, is, destinationFile, projected, timestamp, remoteTimestamp, silent ); } - if ( httpConnection.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED ) + finally { + if (is != null) try { is.close(); } catch (Exception e) { LOG.error("error closing stream", e); } + get.releaseConnection(); + } + + if ( statusCode == HttpURLConnection.HTTP_NOT_FOUND) { - return; + throw new FileNotFoundException(url.toString()); } // test for 401 result (HTTP only) - if ( httpConnection.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED ) + if ( statusCode == HttpURLConnection.HTTP_UNAUTHORIZED ) { throw new IOException( "Not authorized." ); } // test for 407 result (HTTP only) - if ( httpConnection.getResponseCode() == HttpURLConnection.HTTP_PROXY_AUTH ) + if ( statusCode == HttpURLConnection.HTTP_PROXY_AUTH ) { throw new IOException( "Not authorized by proxy." ); } } - - // REVISIT: at this point even non HTTP connections may support the - // if-modified-since behaviour - we just check the date of the - // content and skip the write if it is not newer. - // Some protocols (FTP) dont include dates, of course. - - InputStream is = null; - IOException isException = null; - for ( int i = 0; i < 3; i++ ) + else { - try - { + //set proxy connection + useProxyUser( proxyHost, proxyPort, proxyUserName, proxyPassword ); + + //set up the URL connection + URLConnection connection = source.openConnection(); + InputStream is = null; + try { + //modify the headers + if ( timestamp >= 0 ) + { + connection.setIfModifiedSince( timestamp ); + } + + // prepare Java 1.1 style credentials + if ( username != null || password != null ) + { + String up = username + ":" + password; + String encoding = Base64.encode(up.getBytes(), false); + connection.setRequestProperty( "Authorization", "Basic " + encoding ); + } + + //connect to the remote site (may take some time) + connection.connect(); + + if ( connection.getLastModified() <= timestamp && connection.getLastModified() != 0 ) + { + if (!silent) LOG.info("Not modified"); + return; + } + + long remoteTimestamp = connection.getLastModified(); + is = connection.getInputStream(); - break; + process( true, is, destinationFile, 0, timestamp, remoteTimestamp, silent ); } - catch ( IOException ex ) - { - isException = ex; + finally { + if (is != null) try { is.close(); } catch (Exception e) { LOG.error("error closing stream", e); } } } - if ( is == null ) - { - throw isException; - } - - if ( connection.getLastModified() <= timestamp && - connection.getLastModified() != 0 ) - { - return; - } - - FileOutputStream fos = new FileOutputStream( destinationFile ); + } + private static void process( boolean use, InputStream is, File destinationFile, long projected, long timestamp, long remoteTimestamp, boolean silent ) + throws IOException { byte[] buffer = new byte[100 * 1024]; - int length; + int length, total = 0; + OutputStream os = null; - while ( ( length = is.read( buffer ) ) >= 0 ) - { - fos.write( buffer, 0, length ); - System.out.print( "." ); - } + try { + if ( use ) { + os = new FileOutputStream( destinationFile ); + } + while ( ( length = is.read( buffer ) ) >= 0 ) + { + if ( use ) { + os.write( buffer, 0, length ); + total += length; + if ( !silent ) { + System.out.print( (total/1024) + "/" + (projected == 0 ? "?" : projected + "K" ) + "\r"); + } + } + } - System.out.println(); - fos.close(); - is.close(); - - // if (and only if) the use file time option is set, then the - // saved file now has its timestamp set to that of the downloaded - // file - if ( timestamp >= 0 ) - { - long remoteTimestamp = connection.getLastModified(); - if ( remoteTimestamp != 0 ) - { - touchFile( destinationFile, remoteTimestamp ); + if ( use ) { + if ( !silent ) { + System.out.println( (total/1024) + "K downloaded"); + } + + // if (and only if) the use file time option is set, then the + // saved file now has its timestamp set to that of the downloaded + // file + if ( timestamp >= 0 ) + { + if ( remoteTimestamp != 0 ) + { + touchFile( destinationFile, remoteTimestamp ); + } + } } } + finally { + if (os != null) try { os.close(); } catch (Exception e) { LOG.error("error closing stream", e); } + } } /** @@ -343,7 +539,6 @@ * * @param url The url to parse. * @return The username, password and url. - * @throws RuntimeException if the url is (very) invalid */ static String[] parseUrl( String url ) { @@ -379,7 +574,7 @@ * @param timemillis in milliseconds since the start of the era * @return true if it succeeded. False means that this is a java1.1 system * and that file times can not be set - * @throws RuntimeException Thrown in unrecoverable error. Likely this + * @exception Exception Thrown in unrecoverable error. Likely this * comes from file access failures. */ private static boolean touchFile( File file, long timemillis ) @@ -398,4 +593,6 @@ file.setLastModified( modifiedTime ); return true; } + + private static boolean empty(String s) { return s == null || s.length() == 0; } } No revision No revision 1.34.4.7 +7 -1 maven/src/java/org/apache/maven/verifier/DependencyVerifier.java Index: DependencyVerifier.java =================================================================== RCS file: /home/cvs/maven/src/java/org/apache/maven/verifier/DependencyVerifier.java,v retrieving revision 1.34.4.6 retrieving revision 1.34.4.7 diff -u -r1.34.4.6 -r1.34.4.7 --- DependencyVerifier.java 15 Apr 2004 06:06:18 -0000 1.34.4.6 +++ DependencyVerifier.java 27 Jun 2004 07:57:48 -0000 1.34.4.7 @@ -24,6 +24,7 @@ import org.apache.maven.project.Project; import org.apache.maven.repository.Artifact; import org.apache.maven.util.HttpUtils; +import org.apache.maven.MavenConstants; import java.io.File; import java.io.FileNotFoundException; @@ -311,6 +312,8 @@ try { log.debug( "Getting URL: " + url ); + String loginHost = (String) getProject().getContext().getVariable( MavenConstants.PROXY_LOGINHOST ); + String loginDomain = (String) getProject().getContext().getVariable( MavenConstants.PROXY_LOGINDOMAIN ); HttpUtils.getFile( url, artifact.getFile(), ignoreErrors, @@ -319,6 +322,8 @@ getProject().getContext().getProxyPort(), getProject().getContext().getProxyUserName(), getProject().getContext().getProxyPassword(), + loginHost, + loginDomain, true ); // Artifact was found, continue checking additional remote repos (if any) @@ -355,6 +360,7 @@ // hostnames, or other snafus // FIXME: localize this message log.warn("Error retrieving artifact from [" + url + "]: " + e); + log.debug("Error details", e); } } No revision No revision 1.20.2.5 +31 -1 maven/src/bootstrap/org/apache/maven/BootstrapTask.java Index: BootstrapTask.java =================================================================== RCS file: /home/cvs/maven/src/bootstrap/org/apache/maven/BootstrapTask.java,v retrieving revision 1.20.2.4 retrieving revision 1.20.2.5 diff -u -r1.20.2.4 -r1.20.2.5 --- BootstrapTask.java 10 Mar 2004 11:06:50 -0000 1.20.2.4 +++ BootstrapTask.java 27 Jun 2004 07:57:48 -0000 1.20.2.5 @@ -64,6 +64,10 @@ private String proxyUserName; /** the password to use for the proxy */ private String proxyPassword; + /** the NTLM login host. */ + private String loginHost = null; + /** the NTLM login domain. */ + private String loginDomain = null; /** list of files to process */ private List files; @@ -208,6 +212,32 @@ } /** + * Sets the loginHost attribute of the Get object + * + * @param loginHost the host used to access the NTLM proxy from + */ + public void setLoginHost( String loginHost ) + { + if ( validProperty( loginHost ) ) + { + this.loginHost = loginHost; + } + } + + /** + * Sets the loginDomain attribute of the Get object + * + * @param loginDomain the domain used to access the NTLM proxy on + */ + public void setLoginDomain( String loginDomain ) + { + if ( validProperty( loginDomain ) ) + { + this.loginDomain = loginDomain; + } + } + + /** * Don't stop if get fails if set to "<CODE>true</CODE>". * * @param v if "true" then don't report download errors up to ant @@ -348,7 +378,7 @@ proxyHost, proxyPort, proxyUserName, - proxyPassword ); + proxyPassword, loginHost, loginDomain ); break; } catch ( Exception e ) No revision No revision 1.14.4.24 +3 -0 maven/xdocs/changes.xml Index: changes.xml =================================================================== RCS file: /home/cvs/maven/xdocs/changes.xml,v retrieving revision 1.14.4.23 retrieving revision 1.14.4.24 diff -u -r1.14.4.23 -r1.14.4.24 --- changes.xml 23 Jun 2004 17:12:54 -0000 1.14.4.23 +++ changes.xml 27 Jun 2004 07:57:49 -0000 1.14.4.24 @@ -25,6 +25,9 @@ </properties> <body> <release version="1.0-final-SNAPSHOT" date="in CVS"> + <action dev="brett" type="add" issue="MAVEN-1332" due-to="george wang"> + Support NTLM authentication on a remote repository. + </action> <action dev="evenisse" type="fix" issue="MAVEN-1320" due-to="Miguel Griffa"> Sort all installed plugins. </action> No revision No revision 1.63.4.8 +12 -0 maven/xdocs/reference/user-guide.xml Index: user-guide.xml =================================================================== RCS file: /home/cvs/maven/xdocs/reference/user-guide.xml,v retrieving revision 1.63.4.7 retrieving revision 1.63.4.8 diff -u -r1.63.4.7 -r1.63.4.8 --- user-guide.xml 23 Jun 2004 12:56:19 -0000 1.63.4.7 +++ user-guide.xml 27 Jun 2004 07:57:49 -0000 1.63.4.8 @@ -838,6 +838,18 @@ Password if your proxy requires authentication. </td> </tr> + <tr> + <td>maven.proxy.ntlm.host</td> + <td> + The host to use if you are using NTLM authentication. + </td> + </tr> + <tr> + <td>maven.proxy.ntlm.domain</td> + <td> + The NT domain to use if you are using NTLM authentication. + </td> + </tr> </table> <p>
--------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]