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.configuration.Configurable;
021: import org.apache.avalon.framework.configuration.Configuration;
022: import org.apache.avalon.framework.configuration.ConfigurationException;
023: import org.apache.avalon.framework.logger.AbstractLogEnabled;
024: import org.apache.avalon.framework.service.ServiceException;
025: import org.apache.avalon.framework.service.ServiceManager;
026: import org.apache.avalon.framework.service.Serviceable;
027: import org.apache.james.services.DNSServer;
028: import org.apache.james.util.mail.dsn.DSNStatus;
029: import org.apache.mailet.MailAddress;
030:
031: import java.util.Collection;
032: import java.util.Locale;
033: import java.util.StringTokenizer;
034:
035: /**
036: * Handles MAIL command
037: */
038: public class MailCmdHandler extends AbstractLogEnabled implements
039: CommandHandler, Configurable, Serviceable {
040:
041: private final static String MAIL_OPTION_SIZE = "SIZE";
042:
043: private final static String MESG_SIZE = "MESG_SIZE"; // The size of the message
044:
045: private boolean checkValidSenderDomain = false;
046:
047: private boolean checkAuthClients = false;
048:
049: private DNSServer dnsServer = null;
050:
051: /**
052: * @see org.apache.avalon.framework.configuration.Configurable#configure(Configuration)
053: */
054: public void configure(Configuration handlerConfiguration)
055: throws ConfigurationException {
056: Configuration configuration = handlerConfiguration.getChild(
057: "checkValidSenderDomain", false);
058: if (configuration != null) {
059: checkValidSenderDomain = configuration.getValueAsBoolean();
060: if (checkValidSenderDomain && dnsServer == null) {
061: throw new ConfigurationException(
062: "checkValidSenderDomain enabled but no DNSServer service provided to SMTPServer");
063: }
064: }
065:
066: Configuration configRelay = handlerConfiguration.getChild(
067: "checkAuthClients", false);
068: if (configRelay != null) {
069: checkAuthClients = configRelay.getValueAsBoolean();
070: }
071: }
072:
073: /**
074: * @see org.apache.avalon.framework.service.Serviceable#service(ServiceManager)
075: */
076: public void service(ServiceManager serviceMan)
077: throws ServiceException {
078: dnsServer = (DNSServer) serviceMan.lookup(DNSServer.ROLE);
079: }
080:
081: /**
082: * handles MAIL command
083: *
084: * @see org.apache.james.smtpserver.CommandHandler#onCommand(SMTPSession)
085: */
086: public void onCommand(SMTPSession session) {
087: doMAIL(session, session.getCommandArgument());
088: }
089:
090: /**
091: * Handler method called upon receipt of a MAIL command.
092: * Sets up handler to deliver mail as the stated sender.
093: *
094: * @param session SMTP session object
095: * @param argument the argument passed in with the command by the SMTP client
096: */
097: private void doMAIL(SMTPSession session, String argument) {
098: String responseString = null;
099: StringBuffer responseBuffer = session.getResponseBuffer();
100: String sender = null;
101: boolean badSenderDomain = false;
102:
103: if ((argument != null) && (argument.indexOf(":") > 0)) {
104: int colonIndex = argument.indexOf(":");
105: sender = argument.substring(colonIndex + 1);
106: argument = argument.substring(0, colonIndex);
107: }
108: if (session.getState().containsKey(SMTPSession.SENDER)) {
109: responseString = "503 "
110: + DSNStatus.getStatus(DSNStatus.PERMANENT,
111: DSNStatus.DELIVERY_OTHER)
112: + " Sender already specified";
113: session.writeResponse(responseString);
114: } else if (!session.getState().containsKey(
115: SMTPSession.CURRENT_HELO_MODE)
116: && session.useHeloEhloEnforcement()) {
117: responseString = "503 "
118: + DSNStatus.getStatus(DSNStatus.PERMANENT,
119: DSNStatus.DELIVERY_OTHER)
120: + " Need HELO or EHLO before MAIL";
121: session.writeResponse(responseString);
122: } else if (argument == null
123: || !argument.toUpperCase(Locale.US).equals("FROM")
124: || sender == null) {
125: responseString = "501 "
126: + DSNStatus.getStatus(DSNStatus.PERMANENT,
127: DSNStatus.DELIVERY_INVALID_ARG)
128: + " Usage: MAIL FROM:<sender>";
129: session.writeResponse(responseString);
130: } else {
131: sender = sender.trim();
132: // the next gt after the first lt ... AUTH may add more <>
133: int lastChar = sender.indexOf('>', sender.indexOf('<'));
134: // Check to see if any options are present and, if so, whether they are correctly formatted
135: // (separated from the closing angle bracket by a ' ').
136: if ((lastChar > 0) && (sender.length() > lastChar + 2)
137: && (sender.charAt(lastChar + 1) == ' ')) {
138: String mailOptionString = sender
139: .substring(lastChar + 2);
140:
141: // Remove the options from the sender
142: sender = sender.substring(0, lastChar + 1);
143:
144: StringTokenizer optionTokenizer = new StringTokenizer(
145: mailOptionString, " ");
146: while (optionTokenizer.hasMoreElements()) {
147: String mailOption = optionTokenizer.nextToken();
148: int equalIndex = mailOption.indexOf('=');
149: String mailOptionName = mailOption;
150: String mailOptionValue = "";
151: if (equalIndex > 0) {
152: mailOptionName = mailOption.substring(0,
153: equalIndex).toUpperCase(Locale.US);
154: mailOptionValue = mailOption
155: .substring(equalIndex + 1);
156: }
157:
158: // Handle the SIZE extension keyword
159:
160: if (mailOptionName.startsWith(MAIL_OPTION_SIZE)) {
161: if (!(doMailSize(session, mailOptionValue,
162: sender))) {
163: return;
164: }
165: } else {
166: // Unexpected option attached to the Mail command
167: if (getLogger().isDebugEnabled()) {
168: StringBuffer debugBuffer = new StringBuffer(
169: 128)
170: .append(
171: "MAIL command had unrecognized/unexpected option ")
172: .append(mailOptionName).append(
173: " with value ").append(
174: mailOptionValue);
175: getLogger().debug(debugBuffer.toString());
176: }
177: }
178: }
179: }
180: if (!sender.startsWith("<") || !sender.endsWith(">")) {
181: responseString = "501 "
182: + DSNStatus.getStatus(DSNStatus.PERMANENT,
183: DSNStatus.ADDRESS_SYNTAX_SENDER)
184: + " Syntax error in MAIL command";
185: session.writeResponse(responseString);
186: if (getLogger().isErrorEnabled()) {
187: StringBuffer errorBuffer = new StringBuffer(128)
188: .append("Error parsing sender address: ")
189: .append(sender).append(
190: ": did not start and end with < >");
191: getLogger().error(errorBuffer.toString());
192: }
193: return;
194: }
195: MailAddress senderAddress = null;
196: //Remove < and >
197: sender = sender.substring(1, sender.length() - 1);
198: if (sender.length() == 0) {
199: //This is the <> case. Let senderAddress == null
200: } else {
201:
202: if (sender.indexOf("@") < 0) {
203: sender = sender + "@localhost";
204: }
205:
206: try {
207: senderAddress = new MailAddress(sender);
208: } catch (Exception pe) {
209: responseString = "501 "
210: + DSNStatus.getStatus(DSNStatus.PERMANENT,
211: DSNStatus.ADDRESS_SYNTAX_SENDER)
212: + " Syntax error in sender address";
213: session.writeResponse(responseString);
214: if (getLogger().isErrorEnabled()) {
215: StringBuffer errorBuffer = new StringBuffer(256)
216: .append(
217: "Error parsing sender address: ")
218: .append(sender).append(": ").append(
219: pe.getMessage());
220: getLogger().error(errorBuffer.toString());
221: }
222: return;
223: }
224: }
225:
226: // check only if senderAddress is not null
227: if (checkValidSenderDomain == true && senderAddress != null) {
228:
229: /**
230: * don't check if the ip address is allowed to relay. Only check if it is set in the config.
231: */
232: if (checkAuthClients || !session.isRelayingAllowed()) {
233:
234: // Maybe we should build a static method in org.apache.james.dnsserver.DNSServer ?
235: Collection records;
236:
237: records = dnsServer.findMXRecords(senderAddress
238: .getHost());
239: if (records == null || records.size() == 0) {
240: badSenderDomain = true;
241: }
242:
243: // try to resolv the provided domain in the senderaddress. If it can not resolved do not accept it.
244: if (badSenderDomain) {
245: responseString = "501 "
246: + DSNStatus
247: .getStatus(
248: DSNStatus.PERMANENT,
249: DSNStatus.ADDRESS_SYNTAX_SENDER)
250: + " sender "
251: + senderAddress
252: + " contains a domain with no valid MX records";
253: session.writeResponse(responseString);
254: getLogger().info(responseString);
255: }
256: }
257: }
258:
259: if (!badSenderDomain) {
260: session.getState().put(SMTPSession.SENDER,
261: senderAddress);
262: responseBuffer.append(
263: "250 "
264: + DSNStatus.getStatus(
265: DSNStatus.SUCCESS,
266: DSNStatus.ADDRESS_OTHER)
267: + " Sender <").append(sender).append(
268: "> OK");
269: responseString = session.clearResponseBuffer();
270: session.writeResponse(responseString);
271: }
272: }
273: }
274:
275: /**
276: * Handles the SIZE MAIL option.
277: *
278: * @param session SMTP session object
279: * @param mailOptionValue the option string passed in with the SIZE option
280: * @param tempSender the sender specified in this mail command (for logging purpose)
281: * @return true if further options should be processed, false otherwise
282: */
283: private boolean doMailSize(SMTPSession session,
284: String mailOptionValue, String tempSender) {
285: int size = 0;
286: try {
287: size = Integer.parseInt(mailOptionValue);
288: } catch (NumberFormatException pe) {
289: // This is a malformed option value. We return an error
290: String responseString = "501 "
291: + DSNStatus.getStatus(DSNStatus.PERMANENT,
292: DSNStatus.DELIVERY_INVALID_ARG)
293: + " Syntactically incorrect value for SIZE parameter";
294: session.writeResponse(responseString);
295: getLogger()
296: .error(
297: "Rejected syntactically incorrect value for SIZE parameter.");
298: return false;
299: }
300: if (getLogger().isDebugEnabled()) {
301: StringBuffer debugBuffer = new StringBuffer(128).append(
302: "MAIL command option SIZE received with value ")
303: .append(size).append(".");
304: getLogger().debug(debugBuffer.toString());
305: }
306: long maxMessageSize = session.getConfigurationData()
307: .getMaxMessageSize();
308: if ((maxMessageSize > 0) && (size > maxMessageSize)) {
309: // Let the client know that the size limit has been hit.
310: String responseString = "552 "
311: + DSNStatus.getStatus(DSNStatus.PERMANENT,
312: DSNStatus.SYSTEM_MSG_TOO_BIG)
313: + " Message size exceeds fixed maximum message size";
314: session.writeResponse(responseString);
315: StringBuffer errorBuffer = new StringBuffer(256)
316: .append("Rejected message from ")
317: .append(tempSender != null ? tempSender : null)
318: .append(" from host ")
319: .append(session.getRemoteHost())
320: .append(" (")
321: .append(session.getRemoteIPAddress())
322: .append(") of size ")
323: .append(size)
324: .append(
325: " exceeding system maximum message size of ")
326: .append(maxMessageSize).append(
327: "based on SIZE option.");
328: getLogger().error(errorBuffer.toString());
329: return false;
330: } else {
331: // put the message size in the message state so it can be used
332: // later to restrict messages for user quotas, etc.
333: session.getState().put(MESG_SIZE, new Integer(size));
334: }
335: return true;
336: }
337:
338: }
|