View Javadoc

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