/** * Copyright 2005-2014 Restlet * * The contents of this file are subject to the terms of one of the following * open source licenses: Apache 2.0 or or EPL 1.0 (the "Licenses"). You can * select the license that you prefer but you may not use this file except in * compliance with one of these Licenses. * * You can obtain a copy of the Apache 2.0 license at * http://www.opensource.org/licenses/apache-2.0 * * You can obtain a copy of the EPL 1.0 license at * http://www.opensource.org/licenses/eclipse-1.0 * * See the Licenses for the specific language governing permissions and * limitations under the Licenses. * * Alternatively, you can obtain a royalty free commercial license with less * limitations, transferable or non-transferable, directly at * http://restlet.com/products/restlet-framework * * Restlet is a registered trademark of Restlet S.A.S. */ package org.restlet.ext.javamail; import java.io.IOException; import java.util.Properties; import java.util.logging.Level; import javax.mail.FetchProfile; import javax.mail.Flags; import javax.mail.Folder; import javax.mail.Message; import javax.mail.MessagingException; import javax.mail.NoSuchProviderException; import javax.mail.Session; import javax.mail.Store; import javax.mail.Transport; import javax.mail.UIDFolder; import javax.mail.internet.AddressException; import org.restlet.Client; import org.restlet.Request; import org.restlet.Response; import org.restlet.data.ChallengeScheme; import org.restlet.data.Method; import org.restlet.data.Protocol; import org.restlet.data.Status; import org.restlet.engine.connector.ClientHelper; import org.restlet.representation.Representation; import org.w3c.dom.DOMException; import com.sun.mail.pop3.POP3Folder; /** * Client connector to a mail server. This connector supports the SMTP, SMTP * with STARTTLS and SMTPS protocols to send emails, POP v3 and POPS v3 to * retrieved emails from a mail box.<br> * <br> * To send an email, send a POST request with a resource reference on a SMTP or * SMTPS URI and use an XML email as the entity. A SMTP URI has the following * syntax: smtp://host[:port]<br> * <br> * Use the * {@link Request#setChallengeResponse(org.restlet.data.ChallengeResponse)} * method to set the identified/login and secret/password. You will also need to * specify the {@link ChallengeScheme#SMTP_PLAIN} challenge scheme.<br> * <br> * Sample XML email:<br> * * <pre> * <?xml version="1.0" encoding="ISO-8859-1" ?> * <email> * <head> * <subject>Account activation</subject> * <from>support@restlet.org</from> * <to>user@domain.com</to> * <cc>log@restlet.org</cc> * </head> * <body><![CDATA[Your account was successfully created!]]></body> * </email> * </pre> * * To receive the list of emails, send a GET request to a resource reference on * a POP or POPS URI, leaving the reference path empty. A POP URI has the * following syntax: pop://host[:port]<br> * <br> * Use the * {@link Request#setChallengeResponse(org.restlet.data.ChallengeResponse)} * method to set the identified/login and secret/password. You will also need to * specify the {@link ChallengeScheme#POP_BASIC} or the * {@link ChallengeScheme#POP_DIGEST} challenge scheme.<br> * <br> * Sample XML list of emails:<br> * * <pre> * <?xml version="1.0" encoding="ISO-8859-1" ?> * <emails> * <email href="/1234"/> * <email href="/5678"/> * <email href="/9012"/> * <email href="/3456"/> * </emails> * </pre> * * To retrieve an individual email, just add the href attribute at the end of * the POP URI, such as: pop://host/1234<br> * <br> * Here is the list of parameters that are supported. They should be set in the * Client's context before it is started: * <table> * <tr> * <th>Parameter name</th> * <th>Value type</th> * <th>Default value</th> * <th>Description</th> * </tr> * <tr> * <td>startTls</td> * <td>boolean</td> * <td>false</td> * <td>If true, the SMTP connector will attempt to start a TLS tunnel, right * after the SMTP connection is established.</td> * </tr> * <tr> * <td>debug</td> * <td>boolean</td> * <td>false</td> * <td>If true, the connector will generate JavaMail debug messages.</td> * </tr> * <tr> * <td>representationMessageClass</td> * <td>String</td> * <td>null</td> * <td>If not null, represents the name of a class that extends the JavaMail * "javax.mail.Message" class. This class is able to generate a Message from an * XML representation and a JavaMail Session. The constructor must accept a * {@link Representation} and a JavaMail Session objects as parameters in this * order.</td> * </tr> * </table> * * @author Jerome Louvel */ public class JavaMailClientHelper extends ClientHelper { /** * Constructor. * * @param client * The client to help. */ public JavaMailClientHelper(Client client) { super(client); getProtocols().add(Protocol.SMTP); getProtocols().add(Protocol.SMTPS); getProtocols().add(Protocol.POP); getProtocols().add(Protocol.POPS); } /** * Creates a JavaMail message by parsing an XML representation. * * @param xmlMessage * The XML message to parse. * @param session * The current JavaMail session. * @return The created JavaMail message. * @throws IOException * @throws AddressException * @throws MessagingException */ @SuppressWarnings("unchecked") protected Message createMessage(Representation xmlMessage, Session session) throws IOException, AddressException, MessagingException { final String representationMessageClassName = getRepresentationMessageClass(); if (representationMessageClassName == null) { return new RepresentationMessage(xmlMessage, session); } try { final Class<? extends RepresentationMessage> representationMessageClass = (Class<? extends RepresentationMessage>) Class .forName(representationMessageClassName); return representationMessageClass.getConstructor( Representation.class, Session.class).newInstance( xmlMessage, session); } catch (Exception e) { getLogger().log( Level.SEVERE, "Unable to create a new instance of " + representationMessageClassName, e); return new RepresentationMessage(xmlMessage, session); } } /** * Creates an XML representation based on a JavaMail message. * * @param message * The JavaMail message to format. * @return The XML representation. * @throws DOMException * @throws IOException * @throws MessagingException */ protected Representation createRepresentation(Message message) throws DOMException, IOException, MessagingException { return new MessageRepresentation(message); } /** * Creates an XML representation based on a list of JavaMail messages. * * @param messages * The list of JavaMail messages to format. * @return The XML representation. * @throws IOException * @throws MessagingException */ protected Representation createRepresentation(Message[] messages, POP3Folder inbox) throws IOException, MessagingException { return new MessagesRepresentation(messages, inbox); } /** * Returns the request login. * * @param request * The high-level request. * @return The login. */ private String getLogin(Request request) { if ((request != null) && (request.getChallengeResponse() != null)) { return request.getChallengeResponse().getIdentifier(); } return null; } /** * Returns the request password. * * @param request * The high-level request. * @return The password. */ private String getPassword(Request request) { if ((request != null) && (request.getChallengeResponse() != null)) { return new String(request.getChallengeResponse().getSecret()); } return null; } /** * Returns the full name of the class used for generating JavaMail Message * instances from an XML representation and a JavaMail Session. * * @return The full name of the class used for generating JavaMail Message * instances from an XML representation and a JavaMail Session. */ public String getRepresentationMessageClass() { return getHelpedParameters() .getFirstValue("representationMessageClass"); } @Override public void handle(Request request, Response response) { try { final Protocol protocol = request.getProtocol(); if (Protocol.SMTP.equals(protocol) || Protocol.SMTPS.equals(protocol)) { handleSmtp(request, response); } else if (Protocol.POP.equals(protocol) || Protocol.POPS.equals(protocol)) { handlePop(request, response); } } catch (IOException e) { getLogger().log(Level.WARNING, "JavaMail client error", e); response.setStatus(Status.CONNECTOR_ERROR_INTERNAL, e.getMessage()); } catch (NoSuchProviderException e) { getLogger().log(Level.WARNING, "JavaMail client error", e); response.setStatus(Status.SERVER_ERROR_INTERNAL, e.getMessage()); } catch (AddressException e) { getLogger().log(Level.WARNING, "JavaMail client error", e); response.setStatus(Status.CLIENT_ERROR_BAD_REQUEST, e.getMessage()); } catch (MessagingException e) { getLogger().log(Level.WARNING, "JavaMail client error", e); response.setStatus(Status.SERVER_ERROR_INTERNAL, e.getMessage()); } } /** * Handles a POP or POPS request. * * @param request * The request to handle. * @param response * The response to update. * @throws IOException * @throws MessagingException * @throws IOException */ private void handlePop(Request request, Response response) throws MessagingException, IOException { // Parse the POP URI final String popHost = request.getResourceRef().getHostDomain(); int popPort = request.getResourceRef().getHostPort(); final String path = request.getResourceRef().getPath(); if (popPort == -1) { // No port specified, the default one should be used popPort = request.getProtocol().getDefaultPort(); } if ((popHost == null) || (popHost.equals(""))) { throw new IllegalArgumentException("Invalid POP host specified"); } // Check if authentication required final boolean authenticate = ((getLogin(request) != null) && (getPassword(request) != null)); final boolean apop = authenticate && (ChallengeScheme.POP_DIGEST.equals(request .getChallengeResponse().getScheme())); String transport = null; if (Protocol.POP.equals(request.getProtocol())) { transport = "pop3"; } else if (Protocol.POPS.equals(request.getProtocol())) { transport = "pop3s"; } final Properties props = System.getProperties(); props.put("mail." + transport + ".host", popHost); props.put("mail." + transport + ".port", Integer.toString(popPort)); props.put("mail." + transport + ".apop.enable", Boolean.toString(apop)); // States whether or not to update the folder by removing deleted // messages. boolean updateFolder = false; final Session session = Session.getDefaultInstance(props); session.setDebug(isDebug()); final Store store = session.getStore(transport); store.connect(getLogin(request), getPassword(request)); final POP3Folder inbox = (POP3Folder) store.getFolder("INBOX"); inbox.open(Folder.READ_WRITE); final FetchProfile profile = new FetchProfile(); profile.add(UIDFolder.FetchProfileItem.UID); final Message[] messages = inbox.getMessages(); inbox.fetch(messages, profile); if ((path == null) || path.equals("") || path.equals("/")) { if (Method.GET.equals(request.getMethod()) || Method.HEAD.equals(request.getMethod())) { // Set the result document response.setEntity(createRepresentation(messages, inbox)); } else { response.setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); response.getAllowedMethods().add(Method.GET); response.getAllowedMethods().add(Method.HEAD); } } else if (path.startsWith("/")) { // Retrieve the specified message final String mailUid = path.substring(1); Message message = null; for (int i = 0; (message == null) && (i < messages.length); i++) { final String uid = inbox.getUID(messages[i]); if (mailUid.equals(uid)) { message = messages[i]; } } if (message == null) { // Message not found response.setStatus(Status.CLIENT_ERROR_NOT_FOUND, "No message matches the given UID: " + mailUid); } else { if (Method.GET.equals(request.getMethod()) || Method.HEAD.equals(request.getMethod())) { // Set the result document response.setEntity(createRepresentation(message)); } else if (Method.DELETE.equals(request.getMethod())) { message.setFlag(Flags.Flag.DELETED, true); updateFolder = true; } else { response.setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); response.getAllowedMethods().add(Method.GET); response.getAllowedMethods().add(Method.HEAD); response.getAllowedMethods().add(Method.DELETE); } } } inbox.close(updateFolder); store.close(); } /** * Handles a SMTP or SMTPS request. * * @param request * The request to handle. * @param response * The response to update. * @throws IOException * @throws MessagingException */ private void handleSmtp(Request request, Response response) throws IOException, MessagingException { if (!Method.POST.equals(request.getMethod())) { response.setStatus(Status.CLIENT_ERROR_METHOD_NOT_ALLOWED); response.getAllowedMethods().add(Method.POST); } else { // Parse the SMTP URI final String smtpHost = request.getResourceRef().getHostDomain(); int smtpPort = request.getResourceRef().getHostPort(); if (smtpPort == -1) { // No port specified, the default one should be used smtpPort = request.getProtocol().getDefaultPort(); } if ((smtpHost == null) || (smtpHost.equals(""))) { throw new IllegalArgumentException( "Invalid SMTP host specified"); } // Check if authentication required final boolean authenticate = ((getLogin(request) != null) && (getPassword(request) != null)); String transport = null; if (Protocol.SMTP.equals(request.getProtocol())) { transport = "smtp"; } else if (Protocol.SMTPS.equals(request.getProtocol())) { transport = "smtps"; } final Properties props = System.getProperties(); props.put("mail." + transport + ".host", smtpHost); props.put("mail." + transport + ".port", Integer.toString(smtpPort)); props.put("mail." + transport + ".auth", Boolean.toString(authenticate).toLowerCase()); props.put("mail." + transport + ".starttls.enable", Boolean.toString(isStartTls())); // Open the JavaMail session final Session session = Session.getDefaultInstance(props); session.setDebug(isDebug()); final Transport tr = session.getTransport(transport); if (tr != null) { // Check if authentication is needed if (authenticate) { tr.connect(smtpHost, getLogin(request), getPassword(request)); } else { tr.connect(); } // Actually send the message if (tr.isConnected()) { getLogger() .info("JavaMail client connection successfully established. Attempting to send the message"); // Create the JavaMail message final Message msg = createMessage(request.getEntity(), session); // Send the message tr.sendMessage(msg, msg.getAllRecipients()); tr.close(); getLogger().info( "JavaMail client successfully sent the message."); } } } } /** * Indicates if the connector should generate JavaMail debug messages. * * @return True the connector should generate JavaMail debug messages. */ public boolean isDebug() { return Boolean.parseBoolean(getHelpedParameters().getFirstValue( "debug", "false")); } /** * Indicates if the SMTP protocol should attempt to start a TLS tunnel. * * @return True if the SMTP protocol should attempt to start a TLS tunnel. */ public boolean isStartTls() { return Boolean.parseBoolean(getHelpedParameters().getFirstValue( "startTls", "false")); } }