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