1 /**
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.geronimo.javamail.store.pop3.connection;
19
20 import java.io.*;
21 import java.net.InetAddress;
22 import java.net.Socket;
23 import java.net.SocketException;
24 import java.net.UnknownHostException;
25 import java.security.MessageDigest;
26 import java.security.NoSuchAlgorithmException;
27 import java.util.ArrayList;
28 import java.util.Date;
29 import java.util.HashMap;
30 import java.util.Iterator;
31 import java.util.LinkedList;
32 import java.util.List;
33 import java.util.StringTokenizer;
34
35 import javax.mail.MessagingException;
36 import javax.mail.internet.InternetHeaders;
37
38 import org.apache.geronimo.javamail.authentication.AuthenticatorFactory;
39 import org.apache.geronimo.javamail.authentication.ClientAuthenticator;
40 import org.apache.geronimo.javamail.store.pop3.POP3Constants;
41 import org.apache.geronimo.javamail.util.CommandFailedException;
42 import org.apache.geronimo.javamail.util.InvalidCommandException;
43 import org.apache.geronimo.javamail.util.MIMEInputReader;
44 import org.apache.geronimo.javamail.util.MailConnection;
45 import org.apache.geronimo.javamail.util.ProtocolProperties;
46 import org.apache.geronimo.mail.util.Base64;
47 import org.apache.geronimo.mail.util.Hex;
48
49 /**
50 * Simple implementation of POP3 transport.
51 *
52 * @version $Rev: 597135 $ $Date: 2007-11-21 11:26:57 -0500 (Wed, 21 Nov 2007) $
53 */
54 public class POP3Connection extends MailConnection implements POP3Constants {
55
56 static final protected String MAIL_APOP_ENABLED = "apop.enable";
57 static final protected String MAIL_AUTH_ENABLED = "auth.enable";
58 static final protected String MAIL_RESET_QUIT = "rsetbeforequit";
59 static final protected String MAIL_DISABLE_TOP = "disabletop";
60 static final protected String MAIL_FORGET_TOP = "forgettopheaders";
61
62 // the initial greeting string, which might be required for APOP authentication.
63 protected String greeting;
64 // is use of the AUTH command enabled
65 protected boolean authEnabled;
66 // is use of APOP command enabled
67 protected boolean apopEnabled;
68 // input reader wrapped around the socket input stream
69 protected BufferedReader reader;
70 // output writer wrapped around the socket output stream.
71 protected PrintWriter writer;
72 // this connection was closed unexpectedly
73 protected boolean closed;
74 // indicates whether this conneciton is currently logged in. Once
75 // we send a QUIT, we're finished.
76 protected boolean loggedIn;
77 // indicates whether we need to avoid using the TOP command
78 // when retrieving headers
79 protected boolean topDisabled = false;
80
81 /**
82 * Normal constructor for an POP3Connection() object.
83 *
84 * @param store The store we're associated with (source of parameter values).
85 * @param host The target host name of the IMAP server.
86 * @param port The target listening port of the server. Defaults to 119 if
87 * the port is specified as -1.
88 * @param username The login user name (can be null unless authentication is
89 * required).
90 * @param password Password associated with the userid account. Can be null if
91 * authentication is not required.
92 * @param sslConnection
93 * True if this is targetted as an SSLConnection.
94 * @param debug The session debug flag.
95 */
96 public POP3Connection(ProtocolProperties props) {
97 super(props);
98
99 // get our login properties flags
100 authEnabled = props.getBooleanProperty(MAIL_AUTH_ENABLED, false);
101 apopEnabled = props.getBooleanProperty(MAIL_APOP_ENABLED, false);
102 topDisabled = props.getBooleanProperty(MAIL_DISABLE_TOP, false);
103 }
104
105
106 /**
107 * Connect to the server and do the initial handshaking.
108 *
109 * @exception MessagingException
110 */
111 public boolean protocolConnect(String host, int port, String authid, String realm, String username, String password) throws MessagingException {
112 this.serverHost = host;
113 this.serverPort = port;
114 this.realm = realm;
115 this.authid = authid;
116 this.username = username;
117 this.password = password;
118
119 try {
120 // create socket and connect to server.
121 getConnection();
122 // consume the welcome line
123 getWelcome();
124
125 // go login with the server
126 if (login())
127 {
128 loggedIn = true;
129 return true;
130 }
131 return false;
132 } catch (IOException e) {
133 if (debug) {
134 debugOut("I/O exception establishing connection", e);
135 }
136 throw new MessagingException("Connection error", e);
137 }
138 }
139
140
141 /**
142 * Create a transport connection object and connect it to the
143 * target server.
144 *
145 * @exception MessagingException
146 */
147 protected void getConnection() throws MessagingException
148 {
149 try {
150 // do all of the non-protocol specific set up. This will get our socket established
151 // and ready use.
152 super.getConnection();
153 } catch (IOException e) {
154 throw new MessagingException("Unable to obtain a connection to the POP3 server", e);
155 }
156
157 // The POp3 protocol is inherently a string-based protocol, so we get
158 // string readers/writers for the connection streams
159 reader = new BufferedReader(new InputStreamReader(inputStream));
160 writer = new PrintWriter(new BufferedOutputStream(outputStream));
161 }
162
163 protected void getWelcome() throws IOException {
164 // just read the line and consume it. If debug is
165 // enabled, there I/O stream will be traced
166 greeting = reader.readLine();
167 }
168
169 public String toString() {
170 return "POP3Connection host: " + serverHost + " port: " + serverPort;
171 }
172
173
174 /**
175 * Close the connection. On completion, we'll be disconnected from
176 * the server and unable to send more data.
177 *
178 * @exception MessagingException
179 */
180 public void close() throws MessagingException {
181 // if we're already closed, get outta here.
182 if (socket == null) {
183 return;
184 }
185 try {
186 // say goodbye
187 logout();
188 } finally {
189 // and close up the connection. We do this in a finally block to make sure the connection
190 // is shut down even if quit gets an error.
191 closeServerConnection();
192 // get rid of our response processor too.
193 reader = null;
194 writer = null;
195 }
196 }
197
198
199 /**
200 * Tag this connection as having been closed by the
201 * server. This will not be returned to the
202 * connection pool.
203 */
204 public void setClosed() {
205 closed = true;
206 }
207
208 /**
209 * Test if the connnection has been forcibly closed.
210 *
211 * @return True if the server disconnected the connection.
212 */
213 public boolean isClosed() {
214 return closed;
215 }
216
217 protected POP3Response sendCommand(String cmd) throws MessagingException {
218 return sendCommand(cmd, false);
219 }
220
221 protected POP3Response sendMultiLineCommand(String cmd) throws MessagingException {
222 return sendCommand(cmd, true);
223 }
224
225 protected synchronized POP3Response sendCommand(String cmd, boolean multiLine) throws MessagingException {
226 if (socket.isConnected()) {
227 {
228 // NOTE: We don't use println() because it uses the platform concept of a newline rather
229 // than using CRLF, which is required by the POP3 protocol.
230 writer.write(cmd);
231 writer.write("\r\n");
232 writer.flush();
233
234 POP3Response response = buildResponse(multiLine);
235 if (response.isError()) {
236 throw new CommandFailedException("Error issuing POP3 command: " + cmd);
237 }
238 return response;
239 }
240 }
241 throw new MessagingException("Connection to Mail Server is lost, connection " + this.toString());
242 }
243
244 /**
245 * Build a POP3Response item from the response stream.
246 *
247 * @param isMultiLineResponse
248 * If true, this command is expecting multiple lines back from the server.
249 *
250 * @return A POP3Response item with all of the command response data.
251 * @exception MessagingException
252 */
253 protected POP3Response buildResponse(boolean isMultiLineResponse) throws MessagingException {
254 int status = ERR;
255 byte[] data = null;
256
257 String line;
258 MIMEInputReader source = new MIMEInputReader(reader);
259
260 try {
261 line = reader.readLine();
262 } catch (IOException e) {
263 throw new MessagingException("Error in receving response");
264 }
265
266 if (line == null || line.trim().equals("")) {
267 throw new MessagingException("Empty Response");
268 }
269
270 if (line.startsWith("+OK")) {
271 status = OK;
272 line = removeStatusField(line);
273 if (isMultiLineResponse) {
274 data = getMultiLineResponse();
275 }
276 } else if (line.startsWith("-ERR")) {
277 status = ERR;
278 line = removeStatusField(line);
279 }else if (line.startsWith("+")) {
280 status = CHALLENGE;
281 line = removeStatusField(line);
282 if (isMultiLineResponse) {
283 data = getMultiLineResponse();
284 }
285 } else {
286 throw new MessagingException("Unexpected response: " + line);
287 }
288 return new POP3Response(status, line, data);
289 }
290
291 private static String removeStatusField(String line) {
292 return line.substring(line.indexOf(SPACE) + 1);
293 }
294
295 /**
296 * This could be a multiline response
297 */
298 private byte[] getMultiLineResponse() throws MessagingException {
299
300 MIMEInputReader source = new MIMEInputReader(reader);
301
302 ByteArrayOutputStream out = new ByteArrayOutputStream();
303
304 // it's more efficient to do this a buffer at a time.
305 // the MIMEInputReader takes care of the byte-stuffing and
306 // ".\r\n" input terminator for us.
307 OutputStreamWriter outWriter = new OutputStreamWriter(out);
308 char buffer[] = new char[500];
309 try {
310 int charsRead = -1;
311 while ((charsRead = source.read(buffer)) >= 0) {
312 outWriter.write(buffer, 0, charsRead);
313 }
314 outWriter.flush();
315 } catch (IOException e) {
316 throw new MessagingException("Error processing a multi-line response", e);
317 }
318
319 return out.toByteArray();
320 }
321
322
323 /**
324 * Retrieve the raw message content from the POP3
325 * server. This is all of the message data, including
326 * the header.
327 *
328 * @param sequenceNumber
329 * The message sequence number.
330 *
331 * @return A byte array containing all of the message data.
332 * @exception MessagingException
333 */
334 public byte[] retrieveMessageData(int sequenceNumber) throws MessagingException {
335 POP3Response msgResponse = sendMultiLineCommand("RETR " + sequenceNumber);
336 // we want the data directly in this case.
337 return msgResponse.getData();
338 }
339
340 /**
341 * Retrieve the message header information for a given
342 * message, returned as an input stream suitable
343 * for loading the message data.
344 *
345 * @param sequenceNumber
346 * The server sequence number for the message.
347 *
348 * @return An inputstream that can be used to read the message
349 * data.
350 * @exception MessagingException
351 */
352 public ByteArrayInputStream retrieveMessageHeaders(int sequenceNumber) throws MessagingException {
353 POP3Response msgResponse;
354
355 // some POP3 servers don't correctly implement TOP, so this can be disabled. If
356 // we can't use TOP, then use RETR and retrieve everything. We can just hand back
357 // the stream, as the header loading routine will stop at the first
358 // null line.
359 if (topDisabled) {
360 msgResponse = sendMultiLineCommand("RETR " + sequenceNumber);
361 }
362 else {
363 msgResponse = sendMultiLineCommand("TOP " + sequenceNumber + " 0");
364 }
365
366 // just load the returned message data as a set of headers
367 return msgResponse.getContentStream();
368 }
369
370 /**
371 * Retrieve the total message size from the mail
372 * server. This is the size of the headers plus
373 * the size of the message content.
374 *
375 * @param sequenceNumber
376 * The message sequence number.
377 *
378 * @return The full size of the message.
379 * @exception MessagingException
380 */
381 public int retrieveMessageSize(int sequenceNumber) throws MessagingException {
382 POP3Response msgResponse = sendCommand("LIST " + sequenceNumber);
383 // Convert this into the parsed response type we need.
384 POP3ListResponse list = new POP3ListResponse(msgResponse);
385 // this returns the total message size
386 return list.getSize();
387 }
388
389 /**
390 * Retrieve the mail drop status information.
391 *
392 * @return An object representing the returned mail drop status.
393 * @exception MessagingException
394 */
395 public POP3StatusResponse retrieveMailboxStatus() throws MessagingException {
396 // issue the STAT command and return this into a status response
397 return new POP3StatusResponse(sendCommand("STAT"));
398 }
399
400
401 /**
402 * Retrieve the UID for an individual message.
403 *
404 * @param sequenceNumber
405 * The target message sequence number.
406 *
407 * @return The string UID maintained by the server.
408 * @exception MessagingException
409 */
410 public String retrieveMessageUid(int sequenceNumber) throws MessagingException {
411 POP3Response msgResponse = sendCommand("UIDL " + sequenceNumber);
412
413 String message = msgResponse.getFirstLine();
414 // the UID is everything after the blank separating the message number and the UID.
415 // there's not supposed to be anything else on the message, but trim it of whitespace
416 // just to be on the safe side.
417 return message.substring(message.indexOf(' ') + 1).trim();
418 }
419
420
421 /**
422 * Delete a single message from the mail server.
423 *
424 * @param sequenceNumber
425 * The sequence number of the message to delete.
426 *
427 * @exception MessagingException
428 */
429 public void deleteMessage(int sequenceNumber) throws MessagingException {
430 // just issue the command...we ignore the command response
431 sendCommand("DELE " + sequenceNumber);
432 }
433
434 /**
435 * Logout from the mail server. This sends a QUIT
436 * command, which will likely sever the mail connection.
437 *
438 * @exception MessagingException
439 */
440 public void logout() throws MessagingException {
441 // we may have already sent the QUIT command
442 if (!loggedIn) {
443 return;
444 }
445 // just issue the command...we ignore the command response
446 sendCommand("QUIT");
447 loggedIn = false;
448 }
449
450 /**
451 * Perform a reset on the mail server.
452 *
453 * @exception MessagingException
454 */
455 public void reset() throws MessagingException {
456 // some mail servers mark retrieved messages for deletion
457 // automatically. This will reset the read flags before
458 // we go through normal cleanup.
459 if (props.getBooleanProperty(MAIL_RESET_QUIT, false)) {
460 // just send an RSET command first
461 sendCommand("RSET");
462 }
463 }
464
465 /**
466 * Ping the mail server to see if we still have an active connection.
467 *
468 * @exception MessagingException thrown if we do not have an active connection.
469 */
470 public void pingServer() throws MessagingException {
471 // just issue the command...we ignore the command response
472 sendCommand("NOOP");
473 }
474
475 /**
476 * Login to the mail server, using whichever method is
477 * configured. This will try multiple methods, if allowed,
478 * in decreasing levels of security.
479 *
480 * @return true if the login was successful.
481 * @exception MessagingException
482 */
483 public synchronized boolean login() throws MessagingException {
484 // permitted to use the AUTH command?
485 if (authEnabled) {
486 try {
487 // go do the SASL thing
488 return processSaslAuthentication();
489 } catch (MessagingException e) {
490 // Any error here means fall back to the next mechanism
491 }
492 }
493
494 if (apopEnabled) {
495 try {
496 // go do the SASL thing
497 return processAPOPAuthentication();
498 } catch (MessagingException e) {
499 // Any error here means fall back to the next mechanism
500 }
501 }
502
503 try {
504 // do the tried and true login processing.
505 return processLogin();
506 } catch (MessagingException e) {
507 }
508 // everything failed...can't get in
509 return false;
510 }
511
512
513 /**
514 * Process a basic LOGIN operation, using the
515 * plain test USER/PASS command combo.
516 *
517 * @return true if we logged successfully.
518 * @exception MessagingException
519 */
520 public boolean processLogin() throws MessagingException {
521 // start by sending the USER command, followed by
522 // the PASS command
523 sendCommand("USER " + username);
524 sendCommand("PASS " + password);
525 return true; // we're in
526 }
527
528 /**
529 * Process logging in using the APOP command. Only
530 * works on servers that give a timestamp value
531 * in the welcome response.
532 *
533 * @return true if the login was accepted.
534 * @exception MessagingException
535 */
536 public boolean processAPOPAuthentication() throws MessagingException {
537 int timeStart = greeting.indexOf('<');
538 // if we didn't get an APOP challenge on the greeting, throw an exception
539 // the main login processor will swallow that and fall back to the next
540 // mechanism
541 if (timeStart == -1) {
542 throw new MessagingException("POP3 Server does not support APOP");
543 }
544 int timeEnd = greeting.indexOf('>');
545 String timeStamp = greeting.substring(timeStart, timeEnd + 1);
546
547 // we create the digest password using the timestamp value sent to use
548 // concatenated with the password.
549 String digestPassword = timeStamp + password;
550
551 byte[] digest;
552
553 try {
554 // create a digest value from the password.
555 MessageDigest md = MessageDigest.getInstance("MD5");
556 digest = md.digest(digestPassword.getBytes("iso-8859-1"));
557 } catch (NoSuchAlgorithmException e) {
558 // this shouldn't happen, but if it does, we'll just try a plain
559 // login.
560 throw new MessagingException("Unable to create MD5 digest", e);
561 } catch (UnsupportedEncodingException e) {
562 // this shouldn't happen, but if it does, we'll just try a plain
563 // login.
564 throw new MessagingException("Unable to create MD5 digest", e);
565 }
566 // this will throw an exception if it gives an error failure
567 sendCommand("APOP " + username + " " + Hex.encode(digest));
568 // no exception, we must have passed
569 return true;
570 }
571
572
573 /**
574 * Process SASL-type authentication.
575 *
576 * @return Returns true if the server support a SASL authentication mechanism and
577 * accepted reponse challenges.
578 * @exception MessagingException
579 */
580 protected boolean processSaslAuthentication() throws MessagingException {
581 // if unable to get an appropriate authenticator, just fail it.
582 ClientAuthenticator authenticator = getSaslAuthenticator();
583 if (authenticator == null) {
584 throw new MessagingException("Unable to obtain SASL authenticator");
585 }
586
587 // go process the login.
588 return processLogin(authenticator);
589 }
590
591 /**
592 * Attempt to retrieve a SASL authenticator for this
593 * protocol.
594 *
595 * @return A SASL authenticator, or null if a suitable one
596 * was not located.
597 */
598 protected ClientAuthenticator getSaslAuthenticator() {
599 return AuthenticatorFactory.getAuthenticator(props, selectSaslMechanisms(), serverHost, username, password, authid, realm);
600 }
601
602
603 /**
604 * Process a login using the provided authenticator object.
605 *
606 * NB: This method is synchronized because we have a multi-step process going on
607 * here. No other commands should be sent to the server until we complete.
608 *
609 * @return Returns true if the server support a SASL authentication mechanism and
610 * accepted reponse challenges.
611 * @exception MessagingException
612 */
613 protected synchronized boolean processLogin(ClientAuthenticator authenticator) throws MessagingException {
614 if (debug) {
615 debugOut("Authenticating for user: " + username + " using " + authenticator.getMechanismName());
616 }
617
618 POP3Response response = sendCommand("AUTH " + authenticator.getMechanismName());
619
620 // now process the challenge sequence. We get a continuation response back for each stage of the
621 // authentication, and finally an OK when everything passes muster.
622 while (true) {
623 // this should be a continuation reply, if things are still good.
624 if (response.isChallenge()) {
625 // we're passed back a challenge value, Base64 encoded.
626 byte[] challenge = response.decodeChallengeResponse();
627
628 String responseString = new String(Base64.encode(authenticator.evaluateChallenge(challenge)));
629
630 // have the authenticator evaluate and send back the encoded response.
631 response = sendCommand(responseString);
632 }
633 else {
634 // there are only two choices here, OK or a continuation. OK means
635 // we've passed muster and are in.
636 return true;
637 }
638 }
639 }
640
641
642 /**
643 * Merge the configured SASL mechanisms with the capabilities that the
644 * server has indicated it supports, returning a merged list that can
645 * be used for selecting a mechanism.
646 *
647 * @return A List representing the intersection of the configured list and the
648 * capabilities list.
649 */
650 protected List selectSaslMechanisms() {
651 // just return the set that have been explicity permitted
652 return getSaslMechanisms();
653 }
654 }
655