001: /****************************************************************
002: * Licensed to the Apache Software Foundation (ASF) under one *
003: * or more contributor license agreements. See the NOTICE file *
004: * distributed with this work for additional information *
005: * regarding copyright ownership. The ASF licenses this file *
006: * to you under the Apache License, Version 2.0 (the *
007: * "License"); you may not use this file except in compliance *
008: * with the License. You may obtain a copy of the License at *
009: * *
010: * http://www.apache.org/licenses/LICENSE-2.0 *
011: * *
012: * Unless required by applicable law or agreed to in writing, *
013: * software distributed under the License is distributed on an *
014: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
015: * KIND, either express or implied. See the License for the *
016: * specific language governing permissions and limitations *
017: * under the License. *
018: ****************************************************************/package org.apache.james.smtpserver;
019:
020: import org.apache.avalon.framework.logger.AbstractLogEnabled;
021: import org.apache.james.Constants;
022: import org.apache.james.core.MailHeaders;
023: import org.apache.james.core.MailImpl;
024: import org.apache.james.fetchmail.ReaderInputStream;
025: import org.apache.james.util.CharTerminatedInputStream;
026: import org.apache.james.util.DotStuffingInputStream;
027: import org.apache.james.util.mail.dsn.DSNStatus;
028: import org.apache.james.util.watchdog.BytesReadResetInputStream;
029: import org.apache.mailet.MailAddress;
030: import org.apache.mailet.RFC2822Headers;
031: import org.apache.mailet.dates.RFC822DateFormat;
032:
033: import javax.mail.MessagingException;
034:
035: import java.io.ByteArrayInputStream;
036: import java.io.IOException;
037: import java.io.InputStream;
038: import java.io.SequenceInputStream;
039: import java.io.StringReader;
040: import java.util.Collection;
041: import java.util.Date;
042: import java.util.Enumeration;
043: import java.util.List;
044:
045: /**
046: * handles DATA command
047: */
048: public class DataCmdHandler extends AbstractLogEnabled implements
049: CommandHandler {
050:
051: private final static String SOFTWARE_TYPE = "JAMES SMTP Server "
052: + Constants.SOFTWARE_VERSION;
053:
054: /**
055: * Static RFC822DateFormat used to generate date headers
056: */
057: private final static RFC822DateFormat rfc822DateFormat = new RFC822DateFormat();
058:
059: // Keys used to store/lookup data in the internal state hash map
060:
061: /**
062: * The mail attribute holding the SMTP AUTH user name, if any.
063: */
064: private final static String SMTP_AUTH_USER_ATTRIBUTE_NAME = "org.apache.james.SMTPAuthUser";
065:
066: /**
067: * The character array that indicates termination of an SMTP connection
068: */
069: private final static char[] SMTPTerminator = { '\r', '\n', '.',
070: '\r', '\n' };
071:
072: /**
073: * process DATA command
074: *
075: * @see org.apache.james.smtpserver.CommandHandler#onCommand(SMTPSession)
076: */
077: public void onCommand(SMTPSession session) {
078: doDATA(session, session.getCommandArgument());
079: }
080:
081: /**
082: * Handler method called upon receipt of a DATA command.
083: * Reads in message data, creates header, and delivers to
084: * mail server service for delivery.
085: *
086: * @param session SMTP session object
087: * @param argument the argument passed in with the command by the SMTP client
088: */
089: private void doDATA(SMTPSession session, String argument) {
090: String responseString = null;
091: if ((argument != null) && (argument.length() > 0)) {
092: responseString = "500 "
093: + DSNStatus.getStatus(DSNStatus.PERMANENT,
094: DSNStatus.DELIVERY_INVALID_ARG)
095: + " Unexpected argument provided with DATA command";
096: session.writeResponse(responseString);
097: }
098: if (!session.getState().containsKey(SMTPSession.SENDER)) {
099: responseString = "503 "
100: + DSNStatus.getStatus(DSNStatus.PERMANENT,
101: DSNStatus.DELIVERY_OTHER)
102: + " No sender specified";
103: session.writeResponse(responseString);
104: } else if (!session.getState().containsKey(
105: SMTPSession.RCPT_LIST)) {
106: responseString = "503 "
107: + DSNStatus.getStatus(DSNStatus.PERMANENT,
108: DSNStatus.DELIVERY_OTHER)
109: + " No recipients specified";
110: session.writeResponse(responseString);
111: } else {
112: responseString = "354 Ok Send data ending with <CRLF>.<CRLF>";
113: session.writeResponse(responseString);
114: InputStream msgIn = new CharTerminatedInputStream(session
115: .getInputStream(), SMTPTerminator);
116: try {
117: msgIn = new BytesReadResetInputStream(msgIn, session
118: .getWatchdog(), session.getConfigurationData()
119: .getResetLength());
120:
121: // if the message size limit has been set, we'll
122: // wrap msgIn with a SizeLimitedInputStream
123: long maxMessageSize = session.getConfigurationData()
124: .getMaxMessageSize();
125: if (maxMessageSize > 0) {
126: if (getLogger().isDebugEnabled()) {
127: StringBuffer logBuffer = new StringBuffer(128)
128: .append("Using SizeLimitedInputStream ")
129: .append(" with max message size: ")
130: .append(maxMessageSize);
131: getLogger().debug(logBuffer.toString());
132: }
133: msgIn = new SizeLimitedInputStream(msgIn,
134: maxMessageSize);
135: }
136: // Removes the dot stuffing
137: msgIn = new DotStuffingInputStream(msgIn);
138: // Parse out the message headers
139: MailHeaders headers = new MailHeaders(msgIn);
140: headers = processMailHeaders(session, headers);
141: processMail(session, headers, msgIn);
142: headers = null;
143: } catch (MessagingException me) {
144: // Grab any exception attached to this one.
145: Exception e = me.getNextException();
146: // If there was an attached exception, and it's a
147: // MessageSizeException
148: if (e != null && e instanceof MessageSizeException) {
149: // Add an item to the state to suppress
150: // logging of extra lines of data
151: // that are sent after the size limit has
152: // been hit.
153: session.getState().put(SMTPSession.MESG_FAILED,
154: Boolean.TRUE);
155: // then let the client know that the size
156: // limit has been hit.
157: responseString = "552 "
158: + DSNStatus.getStatus(DSNStatus.PERMANENT,
159: DSNStatus.SYSTEM_MSG_TOO_BIG)
160: + " Error processing message: "
161: + e.getMessage();
162: StringBuffer errorBuffer = new StringBuffer(256)
163: .append("Rejected message from ")
164: .append(
165: session.getState().get(
166: SMTPSession.SENDER)
167: .toString())
168: .append(" from host ")
169: .append(session.getRemoteHost())
170: .append(" (")
171: .append(session.getRemoteIPAddress())
172: .append(
173: ") exceeding system maximum message size of ")
174: .append(
175: session.getConfigurationData()
176: .getMaxMessageSize());
177: getLogger().error(errorBuffer.toString());
178: } else {
179: responseString = "451 "
180: + DSNStatus.getStatus(DSNStatus.TRANSIENT,
181: DSNStatus.UNDEFINED_STATUS)
182: + " Error processing message: "
183: + me.getMessage();
184: getLogger()
185: .error(
186: "Unknown error occurred while processing DATA.",
187: me);
188: }
189: session.writeResponse(responseString);
190: return;
191: } finally {
192: if (msgIn != null) {
193: try {
194: msgIn.close();
195: } catch (Exception e) {
196: // Ignore close exception
197: }
198: msgIn = null;
199: }
200: }
201: }
202: }
203:
204: private MailHeaders processMailHeaders(SMTPSession session,
205: MailHeaders headers) throws MessagingException {
206: // If headers do not contains minimum REQUIRED headers fields,
207: // add them
208: if (!headers.isSet(RFC2822Headers.DATE)) {
209: headers.setHeader(RFC2822Headers.DATE, rfc822DateFormat
210: .format(new Date()));
211: }
212: if (!headers.isSet(RFC2822Headers.FROM)
213: && session.getState().get(SMTPSession.SENDER) != null) {
214: headers.setHeader(RFC2822Headers.FROM, session.getState()
215: .get(SMTPSession.SENDER).toString());
216: }
217: // RFC 2821 says that we cannot examine the message to see if
218: // Return-Path headers are present. If there is one, our
219: // Received: header may precede it, but the Return-Path header
220: // should be removed when making final delivery.
221: // headers.removeHeader(RFC2822Headers.RETURN_PATH);
222: StringBuffer headerLineBuffer = new StringBuffer(512);
223: // We will rebuild the header object to put our Received header at the top
224: Enumeration headerLines = headers.getAllHeaderLines();
225: MailHeaders newHeaders = new MailHeaders();
226: // Put our Received header first
227: headerLineBuffer.append(RFC2822Headers.RECEIVED + ": from ")
228: .append(session.getRemoteHost()).append(" ([").append(
229: session.getRemoteIPAddress()).append("])");
230:
231: newHeaders.addHeaderLine(headerLineBuffer.toString());
232: headerLineBuffer.delete(0, headerLineBuffer.length());
233:
234: headerLineBuffer.append(" by ").append(
235: session.getConfigurationData().getHelloName()).append(
236: " (").append(SOFTWARE_TYPE).append(") with SMTP ID ")
237: .append(session.getSessionID());
238:
239: if (((Collection) session.getState().get(SMTPSession.RCPT_LIST))
240: .size() == 1) {
241: // Only indicate a recipient if they're the only recipient
242: // (prevents email address harvesting and large headers in
243: // bulk email)
244: newHeaders.addHeaderLine(headerLineBuffer.toString());
245: headerLineBuffer.delete(0, headerLineBuffer.length());
246: headerLineBuffer.append(" for <").append(
247: ((List) session.getState().get(
248: SMTPSession.RCPT_LIST)).get(0).toString())
249: .append(">;");
250: newHeaders.addHeaderLine(headerLineBuffer.toString());
251: headerLineBuffer.delete(0, headerLineBuffer.length());
252: } else {
253: // Put the ; on the end of the 'by' line
254: headerLineBuffer.append(";");
255: newHeaders.addHeaderLine(headerLineBuffer.toString());
256: headerLineBuffer.delete(0, headerLineBuffer.length());
257: }
258: headerLineBuffer = null;
259: newHeaders.addHeaderLine(" "
260: + rfc822DateFormat.format(new Date()));
261:
262: // Add all the original message headers back in next
263: while (headerLines.hasMoreElements()) {
264: newHeaders
265: .addHeaderLine((String) headerLines.nextElement());
266: }
267: return newHeaders;
268: }
269:
270: /**
271: * Processes the mail message coming in off the wire. Reads the
272: * content and delivers to the spool.
273: *
274: * @param session SMTP session object
275: * @param headers the headers of the mail being read
276: * @param msgIn the stream containing the message content
277: */
278: private void processMail(SMTPSession session, MailHeaders headers,
279: InputStream msgIn) throws MessagingException {
280: ByteArrayInputStream headersIn = null;
281: MailImpl mail = null;
282: List recipientCollection = null;
283: try {
284: headersIn = new ByteArrayInputStream(headers.toByteArray());
285: recipientCollection = (List) session.getState().get(
286: SMTPSession.RCPT_LIST);
287: mail = new MailImpl(session.getConfigurationData()
288: .getMailServer().getId(), (MailAddress) session
289: .getState().get(SMTPSession.SENDER),
290: recipientCollection, new SequenceInputStream(
291: new SequenceInputStream(headersIn, msgIn),
292: new ReaderInputStream(new StringReader(
293: "\r\n"))));
294: // Call mail.getSize() to force the message to be
295: // loaded. Need to do this to enforce the size limit
296: if (session.getConfigurationData().getMaxMessageSize() > 0) {
297: mail.getMessageSize();
298: }
299: mail.setRemoteHost(session.getRemoteHost());
300: mail.setRemoteAddr(session.getRemoteIPAddress());
301: if (session.getUser() != null) {
302: mail.setAttribute(SMTP_AUTH_USER_ATTRIBUTE_NAME,
303: session.getUser());
304: }
305: session.setMail(mail);
306: } catch (MessagingException me) {
307: // if we get here, it means that we received a
308: // MessagingException, which would happen BEFORE we call
309: // session.setMail, so the mail object is still strictly
310: // local to us, and we really should clean it up before
311: // re-throwing the MessagingException for our call chain
312: // to process.
313: //
314: // So why has this worked at all so far? Initial
315: // conjecture is that it has depended upon finalize to
316: // call dispose. Not in the MailImpl, which doesn't have
317: // one, but even further down in the MimeMessageInputStreamSource.
318:
319: if (mail != null) {
320: mail.dispose();
321: }
322: throw me;
323: } finally {
324: if (recipientCollection != null) {
325: recipientCollection.clear();
326: }
327: recipientCollection = null;
328: if (headersIn != null) {
329: try {
330: headersIn.close();
331: } catch (IOException ioe) {
332: // Ignore exception on close.
333: }
334: }
335: headersIn = null;
336: }
337:
338: }
339:
340: }
|