Author: matthieu
Date: Fri Dec 11 10:06:32 2015
New Revision: 1719311

URL: http://svn.apache.org/viewvc?rev=1719311&view=rev
Log:
JAMES-1644 AuthenticationServlet should use ContinuationTokenManager

Added:
    
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/BadRequestException.java
    
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/InternalErrorException.java
Removed:
    
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/BadRequestException.java
Modified:
    
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/AuthenticationServlet.java
    
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/AccessTokenRequest.java
    
james/project/trunk/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPAuthenticationTest.java

Modified: 
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/AuthenticationServlet.java
URL: 
http://svn.apache.org/viewvc/james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/AuthenticationServlet.java?rev=1719311&r1=1719310&r2=1719311&view=diff
==============================================================================
--- 
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/AuthenticationServlet.java
 (original)
+++ 
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/AuthenticationServlet.java
 Fri Dec 11 10:06:32 2015
@@ -19,7 +19,6 @@
 package org.apache.james.jmap;
 
 import java.io.IOException;
-import java.time.ZonedDateTime;
 
 import javax.inject.Inject;
 import javax.servlet.ServletException;
@@ -27,10 +26,12 @@ import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import org.apache.james.jmap.api.ContinuationTokenManager;
+import org.apache.james.jmap.exceptions.BadRequestException;
+import org.apache.james.jmap.exceptions.InternalErrorException;
 import org.apache.james.jmap.json.MultipleObjectMapperBuilder;
 import org.apache.james.jmap.model.AccessTokenRequest;
 import org.apache.james.jmap.model.AccessTokenResponse;
-import org.apache.james.jmap.model.ContinuationToken;
 import org.apache.james.jmap.model.ContinuationTokenRequest;
 import org.apache.james.jmap.model.ContinuationTokenResponse;
 import org.apache.james.user.api.UsersRepository;
@@ -50,16 +51,19 @@ public class AuthenticationServlet exten
 
     private final ObjectMapper mapper;
     private final UsersRepository usersRepository;
+    private final ContinuationTokenManager continuationTokenManager;
 
     @Inject
-    @VisibleForTesting AuthenticationServlet(UsersRepository usersRepository) {
+    @VisibleForTesting AuthenticationServlet(UsersRepository usersRepository, 
ContinuationTokenManager continuationTokenManager) {
         this.usersRepository = usersRepository;
+        this.continuationTokenManager = continuationTokenManager;
         this.mapper = new MultipleObjectMapperBuilder()
             .registerClass(ContinuationTokenRequest.UNIQUE_JSON_PATH, 
ContinuationTokenRequest.class)
             .registerClass(AccessTokenRequest.UNIQUE_JSON_PATH, 
AccessTokenRequest.class)
             .build();
     }
     
+
     @Override
     protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
throws ServletException, IOException {
         try {
@@ -76,6 +80,9 @@ public class AuthenticationServlet exten
         } catch (BadRequestException e) {
             LOG.warn("Invalid authentication request received.", e);
             resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
+        } catch (InternalErrorException e) {
+            LOG.error("Internal error", e);
+            resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
         }
     }
 
@@ -104,23 +111,37 @@ public class AuthenticationServlet exten
 
     private void handleContinuationTokenRequest(ContinuationTokenRequest 
request, HttpServletResponse resp) throws IOException {
         resp.setContentType(JSON_CONTENT_TYPE_UTF8);
-
-        ContinuationTokenResponse continuationTokenResponse = 
ContinuationTokenResponse
+        try {
+            ContinuationTokenResponse continuationTokenResponse = 
ContinuationTokenResponse
                 .builder()
-                // TODO Answer a real token
-                .continuationToken(new ContinuationToken("fake", 
ZonedDateTime.now(), "fake"))
+                
.continuationToken(continuationTokenManager.generateToken(request.getUsername()))
                 
.methods(ContinuationTokenResponse.AuthenticationMethod.PASSWORD)
                 .build();
-
-        mapper.writeValue(resp.getOutputStream(), continuationTokenResponse);
+            mapper.writeValue(resp.getOutputStream(), 
continuationTokenResponse);
+        } catch (Exception e) {
+            throw new InternalErrorException("Error while responding to 
continuation token");
+        }
     }
 
     private void handleAccessTokenRequest(AccessTokenRequest request, 
HttpServletResponse resp) throws IOException {
-        // TODO get username from continuationToken
-        String username = "username";
+        try {
+            if (!continuationTokenManager.isValid(request.getToken())) {
+                LOG.warn("Use of an invalid ContinuationToken : " + 
request.getToken().serialize());
+                returnUnauthorizedResponse(resp);
+            } else {
+                manageAuthenticationResponse(request, resp);
+            }
+        } catch(Exception e) {
+            throw new InternalErrorException("Internal error while managing 
access token request", e);
+        }
+    }
+
+    private void manageAuthenticationResponse(AccessTokenRequest request, 
HttpServletResponse resp) throws IOException {
+        String username = request.getToken().getUsername();
         if (authenticate(request, username)) {
             returnAccessTokenResponse(resp);
         } else {
+            LOG.info("Authentication failure for " + username);
             returnUnauthorizedResponse(resp);
         }
     }
@@ -151,5 +172,4 @@ public class AuthenticationServlet exten
         resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
     }
 
-
 }

Added: 
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/BadRequestException.java
URL: 
http://svn.apache.org/viewvc/james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/BadRequestException.java?rev=1719311&view=auto
==============================================================================
--- 
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/BadRequestException.java
 (added)
+++ 
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/BadRequestException.java
 Fri Dec 11 10:06:32 2015
@@ -0,0 +1,30 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+package org.apache.james.jmap.exceptions;
+
+public class BadRequestException extends RuntimeException {
+
+    public BadRequestException(String message) {
+        super(message);
+    }
+
+    public BadRequestException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

Added: 
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/InternalErrorException.java
URL: 
http://svn.apache.org/viewvc/james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/InternalErrorException.java?rev=1719311&view=auto
==============================================================================
--- 
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/InternalErrorException.java
 (added)
+++ 
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/InternalErrorException.java
 Fri Dec 11 10:06:32 2015
@@ -0,0 +1,30 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+package org.apache.james.jmap.exceptions;
+
+public class InternalErrorException extends RuntimeException {
+
+    public InternalErrorException(String message) {
+        super(message);
+    }
+
+    public InternalErrorException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}

Modified: 
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/AccessTokenRequest.java
URL: 
http://svn.apache.org/viewvc/james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/AccessTokenRequest.java?rev=1719311&r1=1719310&r2=1719311&view=diff
==============================================================================
--- 
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/AccessTokenRequest.java
 (original)
+++ 
james/project/trunk/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/AccessTokenRequest.java
 Fri Dec 11 10:06:32 2015
@@ -22,8 +22,6 @@ import com.fasterxml.jackson.databind.an
 import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
 import org.apache.james.jmap.exceptions.MalformedContinuationTokenException;
 
-import java.time.DateTimeException;
-
 @JsonDeserialize(builder=AccessTokenRequest.Builder.class)
 public class AccessTokenRequest {
 
@@ -42,7 +40,7 @@ public class AccessTokenRequest {
 
         private Builder() {}
 
-        public Builder token(String token) throws 
MalformedContinuationTokenException, DateTimeException {
+        public Builder token(String token) throws 
MalformedContinuationTokenException {
             this.token = ContinuationToken.fromString(token);
             return this;
         }

Modified: 
james/project/trunk/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPAuthenticationTest.java
URL: 
http://svn.apache.org/viewvc/james/project/trunk/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPAuthenticationTest.java?rev=1719311&r1=1719310&r2=1719311&view=diff
==============================================================================
--- 
james/project/trunk/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPAuthenticationTest.java
 (original)
+++ 
james/project/trunk/server/protocols/jmap/src/test/java/org/apache/james/jmap/JMAPAuthenticationTest.java
 Fri Dec 11 10:06:32 2015
@@ -22,13 +22,19 @@ import static com.jayway.restassured.Res
 import static com.jayway.restassured.RestAssured.with;
 import static com.jayway.restassured.config.EncoderConfig.encoderConfig;
 import static com.jayway.restassured.config.RestAssuredConfig.newConfig;
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.hamcrest.Matchers.hasItem;
 import static org.hamcrest.Matchers.isA;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import org.apache.james.http.jetty.Configuration;
 import org.apache.james.http.jetty.JettyHttpServer;
+import org.apache.james.jmap.api.ContinuationTokenManager;
+import org.apache.james.jmap.model.ContinuationToken;
+import org.apache.james.jmap.utils.ZonedDateTimeProvider;
 import org.apache.james.user.api.UsersRepository;
 import org.apache.james.user.api.UsersRepositoryException;
 import org.junit.After;
@@ -39,15 +45,26 @@ import com.google.common.base.Charsets;
 import com.jayway.restassured.RestAssured;
 import com.jayway.restassured.http.ContentType;
 
-public class JMAPAuthenticationTest {
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
 
+public class JMAPAuthenticationTest {
+       
+       private static final ZonedDateTime oldDate = 
ZonedDateTime.parse("2011-12-03T10:15:30+01:00", 
DateTimeFormatter.ISO_OFFSET_DATE_TIME);
+       private static final ZonedDateTime newDate = 
ZonedDateTime.parse("2011-12-03T10:16:30+01:00", 
DateTimeFormatter.ISO_OFFSET_DATE_TIME);
+       
     private JettyHttpServer server;
     private UsersRepository mockedUsersRepository;
+    private ContinuationTokenManager mockedContinuationTokenManager;
+    private ZonedDateTimeProvider mockedZonedDateTimeProvider;
     
     @Before
     public void setup() throws Exception {
         mockedUsersRepository = mock(UsersRepository.class);
-        AuthenticationServlet authenticationServlet = new 
AuthenticationServlet(mockedUsersRepository);
+        mockedContinuationTokenManager = mock(ContinuationTokenManager.class);
+        mockedZonedDateTimeProvider = mock(ZonedDateTimeProvider.class);
+        
+        AuthenticationServlet authenticationServlet = new 
AuthenticationServlet(mockedUsersRepository, mockedContinuationTokenManager);
         
         server = JettyHttpServer.create(
                 Configuration.builder()
@@ -141,7 +158,11 @@ public class JMAPAuthenticationTest {
     }
 
     @Test
-    public void mustReturnJsonResponse() {
+    public void mustReturnJsonResponse() throws Exception {
+        
when(mockedContinuationTokenManager.generateToken(eq("[email protected]")))
+            .thenAnswer(invocationOnMock -> new 
ContinuationToken("[email protected]", newDate, "signature"));
+        
when(mockedContinuationTokenManager.isValid(any())).thenAnswer(invocationOnMock 
-> true);
+
         given()
             .contentType(ContentType.JSON)
             .accept(ContentType.JSON)
@@ -152,6 +173,7 @@ public class JMAPAuthenticationTest {
             .statusCode(200)
             .contentType(ContentType.JSON);
     }
+    
 
 
     @Test
@@ -167,7 +189,11 @@ public class JMAPAuthenticationTest {
     }
 
     @Test
-    public void methodShouldContainPasswordWhenValidResquest() {
+    public void methodShouldContainPasswordWhenValidResquest() throws 
Exception {
+        
when(mockedZonedDateTimeProvider.provide()).thenAnswer(invocationOnMock -> 
oldDate);
+        
when(mockedContinuationTokenManager.generateToken(eq("[email protected]")))
+            .thenAnswer(invocationOnMock -> new 
ContinuationToken("[email protected]", newDate, "signature"));
+
         given()
             .contentType(ContentType.JSON)
             .accept(ContentType.JSON)
@@ -180,7 +206,11 @@ public class JMAPAuthenticationTest {
     }
 
     @Test
-    public void mustReturnContinuationTokenWhenValidResquest() {
+    public void mustReturnContinuationTokenWhenValidResquest() throws 
Exception {
+        
when(mockedZonedDateTimeProvider.provide()).thenAnswer(invocationOnMock -> 
oldDate);
+        
when(mockedContinuationTokenManager.generateToken(eq("[email protected]")))
+            .thenAnswer(invocationOnMock -> new 
ContinuationToken("[email protected]", newDate, "signature"));
+
         given()
             .contentType(ContentType.JSON)
             .accept(ContentType.JSON)
@@ -193,17 +223,22 @@ public class JMAPAuthenticationTest {
     }
 
     @Test
-    public void mustReturnAuthenticationFailedWhenBadPassword() {
-        String continuationToken =
-                with()
-                    .contentType(ContentType.JSON)
-                    .accept(ContentType.JSON)
-                    .body("{\"username\": \"[email protected]\", \"clientName\": 
\"Mozilla Thunderbird\", \"clientVersion\": \"42.0\", \"deviceName\": \"Joe 
Blogg’s iPhone\"}")
-                .post("/authentication")
-                    .body()
-                    .path("continuationToken")
-                .toString();
+    public void mustReturnAuthenticationFailedWhenBadPassword() throws 
Exception {
+        
when(mockedZonedDateTimeProvider.provide()).thenAnswer(invocationOnMock -> 
oldDate);
+        
when(mockedContinuationTokenManager.generateToken(eq("[email protected]")))
+            .thenAnswer(invocationOnMock -> new 
ContinuationToken("[email protected]", newDate, "signature"));
+        
when(mockedContinuationTokenManager.isValid(any())).thenAnswer(invocationOnMock 
-> true);
 
+        String continuationToken =
+        with()
+            .contentType(ContentType.JSON)
+            .accept(ContentType.JSON)
+            .body("{\"username\": \"[email protected]\", \"clientName\": 
\"Mozilla Thunderbird\", \"clientVersion\": \"42.0\", \"deviceName\": \"Joe 
Blogg’s iPhone\"}")
+        .post("/authentication")
+            .body()
+            .path("continuationToken")
+            .toString();
+        
         given()
             .contentType(ContentType.JSON)
             .accept(ContentType.JSON)
@@ -216,19 +251,24 @@ public class JMAPAuthenticationTest {
 
     @Test
     public void mustReturnAuthenticationFailedWhenUsersRepositoryException() 
throws Exception {
-        String continuationToken =
-                with()
-                    .contentType(ContentType.JSON)
-                    .accept(ContentType.JSON)
-                    .body("{\"username\": \"[email protected]\", \"clientName\": 
\"Mozilla Thunderbird\", \"clientVersion\": \"42.0\", \"deviceName\": \"Joe 
Blogg’s iPhone\"}")
-                .post("/authentication")
-                    .body()
-                .path("continuationToken")
-                .toString();
+        
when(mockedZonedDateTimeProvider.provide()).thenAnswer(invocationOnMock -> 
oldDate);
+        
when(mockedContinuationTokenManager.generateToken(eq("[email protected]")))
+            .thenAnswer(invocationOnMock -> new 
ContinuationToken("[email protected]", newDate, "signature"));
+        
when(mockedContinuationTokenManager.isValid(any())).thenAnswer(invocationOnMock 
-> true);
 
+        String continuationToken =
+        with()
+            .contentType(ContentType.JSON)
+            .accept(ContentType.JSON)
+            .body("{\"username\": \"[email protected]\", \"clientName\": 
\"Mozilla Thunderbird\", \"clientVersion\": \"42.0\", \"deviceName\": \"Joe 
Blogg’s iPhone\"}")
+        .post("/authentication")
+            .body()
+            .path("continuationToken")
+            .toString();
+        
         when(mockedUsersRepository.test("username", "password"))
             .thenThrow(new UsersRepositoryException("test"));
-
+    
         given()
             .contentType(ContentType.JSON)
             .accept(ContentType.JSON)
@@ -241,19 +281,24 @@ public class JMAPAuthenticationTest {
 
     @Test
     public void mustReturnCreatedWhenGoodPassword() throws Exception {
-        String continuationToken =
-                with()
-                    .contentType(ContentType.JSON)
-                    .accept(ContentType.JSON)
-                    .body("{\"username\": \"[email protected]\", \"clientName\": 
\"Mozilla Thunderbird\", \"clientVersion\": \"42.0\", \"deviceName\": \"Joe 
Blogg’s iPhone\"}")
-                .post("/authentication")
-                    .body()
-                .path("continuationToken")
-                .toString();
+        
when(mockedZonedDateTimeProvider.provide()).thenAnswer(invocationOnMock -> 
oldDate);
+        
when(mockedContinuationTokenManager.generateToken(eq("[email protected]")))
+            .thenAnswer(invocationOnMock -> new ContinuationToken("username", 
newDate, "signature"));
+        
when(mockedContinuationTokenManager.isValid(any())).thenAnswer(invocationOnMock 
-> true);
 
+        String continuationToken =
+        with()
+            .contentType(ContentType.JSON)
+            .accept(ContentType.JSON)
+            .body("{\"username\": \"[email protected]\", \"clientName\": 
\"Mozilla Thunderbird\", \"clientVersion\": \"42.0\", \"deviceName\": \"Joe 
Blogg’s iPhone\"}")
+        .post("/authentication")
+            .body()
+            .path("continuationToken")
+            .toString();
+        
         when(mockedUsersRepository.test("username", "password"))
             .thenReturn(true);
-
+        
         given()
             .contentType(ContentType.JSON)
             .accept(ContentType.JSON)
@@ -266,27 +311,51 @@ public class JMAPAuthenticationTest {
 
     @Test
     public void mustSendJsonContainingAccessTokenWhenGoodPassword() throws 
Exception {
+        
when(mockedZonedDateTimeProvider.provide()).thenAnswer(invocationOnMock -> 
oldDate);
+        ContinuationToken sentContinuationToken = new 
ContinuationToken("[email protected]", newDate, "signature");
+        
when(mockedContinuationTokenManager.generateToken(eq("[email protected]")))
+            .thenAnswer(invocationOnMock -> sentContinuationToken);
+        
when(mockedContinuationTokenManager.isValid(any())).thenAnswer(invocationOnMock 
-> true);
+
+        ContinuationToken receivedContinuationToken = 
ContinuationToken.fromString(
+            with()
+                .contentType(ContentType.JSON)
+                .accept(ContentType.JSON)
+                .body("{\"username\": \"[email protected]\", \"clientName\": 
\"Mozilla Thunderbird\", \"clientVersion\": \"42.0\", \"deviceName\": \"Joe 
Blogg’s iPhone\"}")
+            .post("/authentication")
+                .body()
+                .path("continuationToken")
+                .toString());
+
+        assertThat(receivedContinuationToken).isEqualTo(sentContinuationToken);
+    }
+    
+
+    @Test
+    public void 
mustReturnAuthenticationFailedWhenContinuationTokenIsRejectedByTheContinuationTokenManager()
 throws Exception {
+        
when(mockedZonedDateTimeProvider.provide()).thenAnswer(invocationOnMock -> 
oldDate);
+        
when(mockedContinuationTokenManager.generateToken(eq("[email protected]")))
+            .thenAnswer(invocationOnMock -> new 
ContinuationToken("[email protected]", newDate, "signature"));
+        
when(mockedContinuationTokenManager.isValid(any())).thenAnswer(invocationOnMock 
-> false);
+
         String continuationToken =
                 with()
                     .contentType(ContentType.JSON)
                     .accept(ContentType.JSON)
                     .body("{\"username\": \"[email protected]\", \"clientName\": 
\"Mozilla Thunderbird\", \"clientVersion\": \"42.0\", \"deviceName\": \"Joe 
Blogg’s iPhone\"}")
-                .post("/authentication")
+                    .post("/authentication")
                     .body()
-                .path("continuationToken")
+                    .path("continuationToken")
                 .toString();
 
-        when(mockedUsersRepository.test("username", "password"))
-            .thenReturn(true);
-
         given()
             .contentType(ContentType.JSON)
             .accept(ContentType.JSON)
-            .body("{\"token\": \"" + continuationToken + "\", \"method\": 
\"password\", \"password\": \"password\"}")
+            .body("{\"token\": \"" + continuationToken + "\", \"method\": 
\"password\", \"password\": \"badpassword\"}")
         .when()
             .post("/authentication")
         .then()
-            .contentType(ContentType.JSON)
-            .body("accessToken", isA(String.class));
+            .statusCode(401);
     }
+
 }



---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to