View Javadoc

1   /****************************************************************
2    * Licensed to the Apache Software Foundation (ASF) under one   *
3    * or more contributor license agreements.  See the NOTICE file *
4    * distributed with this work for additional information        *
5    * regarding copyright ownership.  The ASF licenses this file   *
6    * to you under the Apache License, Version 2.0 (the            *
7    * "License"); you may not use this file except in compliance   *
8    * with the License.  You may obtain a copy of the License at   *
9    *                                                              *
10   *   http://www.apache.org/licenses/LICENSE-2.0                 *
11   *                                                              *
12   * Unless required by applicable law or agreed to in writing,   *
13   * software distributed under the License is distributed on an  *
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15   * KIND, either express or implied.  See the License for the    *
16   * specific language governing permissions and limitations      *
17   * under the License.                                           *
18   ****************************************************************/
19  
20  
21  
22  package org.apache.james.smtpserver.core;
23  
24  import org.apache.avalon.framework.logger.AbstractLogEnabled;
25  import org.apache.james.Constants;
26  import org.apache.james.core.MailHeaders;
27  import org.apache.james.core.MailImpl;
28  import org.apache.james.dsn.DSNStatus;
29  import org.apache.james.smtpserver.ReaderInputStream;
30  import org.apache.james.smtpserver.CommandHandler;
31  import org.apache.james.smtpserver.MessageSizeException;
32  import org.apache.james.smtpserver.SMTPSession;
33  import org.apache.james.smtpserver.SizeLimitedInputStream;
34  import org.apache.james.util.stream.CharTerminatedInputStream;
35  import org.apache.james.util.stream.DotStuffingInputStream;
36  import org.apache.james.util.watchdog.BytesReadResetInputStream;
37  import org.apache.mailet.MailAddress;
38  import org.apache.mailet.base.RFC2822Headers;
39  import org.apache.mailet.base.RFC822DateFormat;
40  
41  import javax.mail.MessagingException;
42  
43  import java.io.ByteArrayInputStream;
44  import java.io.IOException;
45  import java.io.InputStream;
46  import java.io.SequenceInputStream;
47  import java.io.StringReader;
48  import java.util.ArrayList;
49  import java.util.Collection;
50  import java.util.Date;
51  import java.util.Enumeration;
52  import java.util.List;
53  
54  
55  /**
56    * handles DATA command
57   */
58  public class DataCmdHandler
59      extends AbstractLogEnabled
60      implements CommandHandler {
61  
62      private final static String SOFTWARE_TYPE = "JAMES SMTP Server "
63                                                   + Constants.SOFTWARE_VERSION;
64  
65      /**
66       * Static RFC822DateFormat used to generate date headers
67       */
68      private final static RFC822DateFormat rfc822DateFormat = new RFC822DateFormat();
69  
70      // Keys used to store/lookup data in the internal state hash map
71  
72      /**
73       * The mail attribute holding the SMTP AUTH user name, if any.
74       */
75      private final static String SMTP_AUTH_USER_ATTRIBUTE_NAME = "org.apache.james.SMTPAuthUser";
76  
77      /**
78       * The mail attribute which get set if the client is allowed to relay
79       */
80      private final static String SMTP_AUTH_NETWORK_NAME = "org.apache.james.SMTPIsAuthNetwork";
81  
82      /**
83       * The character array that indicates termination of an SMTP connection
84       */
85      private final static char[] SMTPTerminator = { '\r', '\n', '.', '\r', '\n' };
86  
87      /**
88       * process DATA command
89       *
90       * @see org.apache.james.smtpserver.CommandHandler#onCommand(SMTPSession)
91       */
92      public void onCommand(SMTPSession session) {
93          doDATA(session, session.getCommandArgument());
94      }
95  
96  
97      /**
98       * Handler method called upon receipt of a DATA command.
99       * Reads in message data, creates header, and delivers to
100      * mail server service for delivery.
101      *
102      * @param session SMTP session object
103      * @param argument the argument passed in with the command by the SMTP client
104      */
105     private void doDATA(SMTPSession session, String argument) {
106 
107         String responseString = null;
108         responseString = "354 Ok Send data ending with <CRLF>.<CRLF>";
109         session.writeResponse(responseString);
110         InputStream msgIn = new CharTerminatedInputStream(session
111                 .getInputStream(), SMTPTerminator);
112         try {
113             msgIn = new BytesReadResetInputStream(msgIn, session.getWatchdog(),
114                     session.getConfigurationData().getResetLength());
115 
116             // if the message size limit has been set, we'll
117             // wrap msgIn with a SizeLimitedInputStream
118             long maxMessageSize = session.getConfigurationData()
119                     .getMaxMessageSize();
120             if (maxMessageSize > 0) {
121                 if (getLogger().isDebugEnabled()) {
122                     StringBuffer logBuffer = new StringBuffer(128).append(
123                             "Using SizeLimitedInputStream ").append(
124                             " with max message size: ").append(maxMessageSize);
125                     getLogger().debug(logBuffer.toString());
126                 }
127                 msgIn = new SizeLimitedInputStream(msgIn, maxMessageSize);
128             }
129             // Removes the dot stuffing
130             msgIn = new DotStuffingInputStream(msgIn);
131             // Parse out the message headers
132             MailHeaders headers = new MailHeaders(msgIn);
133             headers = processMailHeaders(session, headers);
134             processMail(session, headers, msgIn);
135             headers = null;
136         } catch (MessagingException me) {
137             // Grab any exception attached to this one.
138             Exception e = me.getNextException();
139             // If there was an attached exception, and it's a
140             // MessageSizeException
141             if (e != null && e instanceof MessageSizeException) {
142                 // Add an item to the state to suppress
143                 // logging of extra lines of data
144                 // that are sent after the size limit has
145                 // been hit.
146                 session.getState().put(SMTPSession.MESG_FAILED, Boolean.TRUE);
147                 // then let the client know that the size
148                 // limit has been hit.
149                 responseString = "552 "
150                         + DSNStatus.getStatus(DSNStatus.PERMANENT,
151                                 DSNStatus.SYSTEM_MSG_TOO_BIG)
152                         + " Error processing message: " + e.getMessage();
153                 StringBuffer errorBuffer = new StringBuffer(256).append(
154                         "Rejected message from ").append(
155                         session.getState().get(SMTPSession.SENDER).toString())
156                         .append(" from host ").append(session.getRemoteHost())
157                         .append(" (").append(session.getRemoteIPAddress())
158                         .append(") exceeding system maximum message size of ")
159                         .append(
160                                 session.getConfigurationData()
161                                         .getMaxMessageSize());
162                 getLogger().error(errorBuffer.toString());
163             } else {
164                 responseString = "451 "
165                         + DSNStatus.getStatus(DSNStatus.TRANSIENT,
166                                 DSNStatus.UNDEFINED_STATUS)
167                         + " Error processing message: " + me.getMessage();
168                 getLogger().error(
169                         "Unknown error occurred while processing DATA.", me);
170             }
171             session.writeResponse(responseString);
172             return;
173         } finally {
174             if (msgIn != null) {
175                 try {
176                     msgIn.close();
177                 } catch (Exception e) {
178                     // Ignore close exception
179                 }
180                 msgIn = null;
181             }
182         }
183 
184     }
185 
186 
187 
188 
189 
190 
191 
192     private MailHeaders processMailHeaders(SMTPSession session, MailHeaders headers)
193         throws MessagingException {
194         // If headers do not contains minimum REQUIRED headers fields,
195         // add them
196         if (!headers.isSet(RFC2822Headers.DATE)) {
197             headers.setHeader(RFC2822Headers.DATE, rfc822DateFormat.format(new Date()));
198         }
199         if (!headers.isSet(RFC2822Headers.FROM) && session.getState().get(SMTPSession.SENDER) != null) {
200             headers.setHeader(RFC2822Headers.FROM, session.getState().get(SMTPSession.SENDER).toString());
201         }
202         // RFC 2821 says that we cannot examine the message to see if
203         // Return-Path headers are present.  If there is one, our
204         // Received: header may precede it, but the Return-Path header
205         // should be removed when making final delivery.
206      // headers.removeHeader(RFC2822Headers.RETURN_PATH);
207         StringBuffer headerLineBuffer = new StringBuffer(512);
208         // We will rebuild the header object to put our Received header at the top
209         Enumeration headerLines = headers.getAllHeaderLines();
210         MailHeaders newHeaders = new MailHeaders();
211         
212         String heloMode = (String) session.getConnectionState().get(SMTPSession.CURRENT_HELO_MODE);
213         String heloName = (String) session.getConnectionState().get(SMTPSession.CURRENT_HELO_NAME);
214 
215         // Put our Received header first
216         headerLineBuffer.append(RFC2822Headers.RECEIVED + ": from ")
217                         .append(session.getRemoteHost());
218         
219         if (heloName != null) {
220             headerLineBuffer.append(" (")
221                             .append(heloMode)
222                             .append(" ")
223                             .append(heloName)
224                             .append(") ");
225         }
226         
227         headerLineBuffer.append(" ([")
228                         .append(session.getRemoteIPAddress())
229                         .append("])");
230 
231         newHeaders.addHeaderLine(headerLineBuffer.toString());
232         headerLineBuffer.delete(0, headerLineBuffer.length());
233 
234         headerLineBuffer.append("          by ")
235                         .append(session.getConfigurationData().getHelloName())
236                         .append(" (")
237                         .append(SOFTWARE_TYPE)
238                         .append(") with ");
239      
240         // Check if EHLO was used 
241         if ("EHLO".equals(heloMode)) {
242             
243             // Not succesfull auth
244             if (session.getUser() == null) {
245                 headerLineBuffer.append("ESMTP");  
246             } else {
247                 // See RFC3848
248                 // The new keyword "ESMTPA" indicates the use of ESMTP when the SMTP
249                 // AUTH [3] extension is also used and authentication is successfully
250                 // achieved.
251                 headerLineBuffer.append("ESMTPA");
252             }
253         } else {
254             headerLineBuffer.append("SMTP");
255         }
256         
257         headerLineBuffer.append(" ID ")
258                         .append(session.getSessionID());
259 
260         if (((Collection) session.getState().get(SMTPSession.RCPT_LIST)).size() == 1) {
261             // Only indicate a recipient if they're the only recipient
262             // (prevents email address harvesting and large headers in
263             //  bulk email)
264             newHeaders.addHeaderLine(headerLineBuffer.toString());
265             headerLineBuffer.delete(0, headerLineBuffer.length());
266             headerLineBuffer.append("          for <")
267                             .append(((List) session.getState().get(SMTPSession.RCPT_LIST)).get(0).toString())
268                             .append(">;");
269             newHeaders.addHeaderLine(headerLineBuffer.toString());
270             headerLineBuffer.delete(0, headerLineBuffer.length());
271         } else {
272             // Put the ; on the end of the 'by' line
273             headerLineBuffer.append(";");
274             newHeaders.addHeaderLine(headerLineBuffer.toString());
275             headerLineBuffer.delete(0, headerLineBuffer.length());
276         }
277         headerLineBuffer = null;
278         newHeaders.addHeaderLine("          " + rfc822DateFormat.format(new Date()));
279 
280         // Add all the original message headers back in next
281         while (headerLines.hasMoreElements()) {
282             newHeaders.addHeaderLine((String) headerLines.nextElement());
283         }
284         return newHeaders;
285     }
286 
287     /**
288      * Processes the mail message coming in off the wire.  Reads the
289      * content and delivers to the spool.
290      *
291      * @param session SMTP session object
292      * @param headers the headers of the mail being read
293      * @param msgIn the stream containing the message content
294      */
295     private void processMail(SMTPSession session, MailHeaders headers, InputStream msgIn)
296         throws MessagingException {
297         ByteArrayInputStream headersIn = null;
298         MailImpl mail = null;
299         List recipientCollection = null;
300         try {
301             headersIn = new ByteArrayInputStream(headers.toByteArray());
302             recipientCollection = (List) session.getState().get(SMTPSession.RCPT_LIST);
303             mail =
304                 new MailImpl(session.getConfigurationData().getMailServer().getId(),
305                              (MailAddress) session.getState().get(SMTPSession.SENDER),
306                              recipientCollection,
307                              new SequenceInputStream(new SequenceInputStream(headersIn, msgIn),
308                                      new ReaderInputStream(new StringReader("\r\n"))));
309             // Call mail.getSize() to force the message to be
310             // loaded. Need to do this to enforce the size limit
311             if (session.getConfigurationData().getMaxMessageSize() > 0) {
312                 mail.getMessageSize();
313             }
314             mail.setRemoteHost(session.getRemoteHost());
315             mail.setRemoteAddr(session.getRemoteIPAddress());
316             if (session.getUser() != null) {
317                 mail.setAttribute(SMTP_AUTH_USER_ATTRIBUTE_NAME, session.getUser());
318             }
319             
320             if (session.isRelayingAllowed()) {
321                 mail.setAttribute(SMTP_AUTH_NETWORK_NAME,"true");
322             }
323             
324             session.setMail(mail);
325         } catch (MessagingException me) {
326             // if we get here, it means that we received a
327             // MessagingException, which would happen BEFORE we call
328             // session.setMail, so the mail object is still strictly
329             // local to us, and we really should clean it up before
330             // re-throwing the MessagingException for our call chain
331             // to process.
332             //
333             // So why has this worked at all so far?  Initial
334             // conjecture is that it has depended upon finalize to
335             // call dispose.  Not in the MailImpl, which doesn't have
336             // one, but even further down in the MimeMessageInputStreamSource.
337 
338             if (mail != null) {
339                 mail.dispose();
340             }
341             throw me;
342         } finally {
343             if (recipientCollection != null) {
344                 recipientCollection.clear();
345             }
346             recipientCollection = null;
347             if (headersIn != null) {
348                 try {
349                     headersIn.close();
350                 } catch (IOException ioe) {
351                     // Ignore exception on close.
352                 }
353             }
354             headersIn = null;
355         }
356 
357     }
358     
359     /**
360      * @see org.apache.james.smtpserver.CommandHandler#getImplCommands()
361      */
362     public Collection getImplCommands() {
363         Collection implCommands = new ArrayList();
364         implCommands.add("DATA");
365         
366         return implCommands;
367     }
368 
369 }