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.configuration.Configurable;
21  import org.apache.avalon.framework.configuration.Configuration;
22  import org.apache.avalon.framework.configuration.ConfigurationException;
23  import org.apache.avalon.framework.logger.AbstractLogEnabled;
24  import org.apache.avalon.framework.service.ServiceException;
25  import org.apache.avalon.framework.service.ServiceManager;
26  import org.apache.avalon.framework.service.Serviceable;
27  import org.apache.james.services.DNSServer;
28  import org.apache.james.util.mail.dsn.DSNStatus;
29  import org.apache.mailet.MailAddress;
30  
31  import java.util.Collection;
32  import java.util.Locale;
33  import java.util.StringTokenizer;
34  
35  /***
36    * Handles MAIL command
37    */
38  public class MailCmdHandler
39      extends AbstractLogEnabled
40      implements CommandHandler,Configurable, Serviceable {
41  
42      private final static String MAIL_OPTION_SIZE = "SIZE";
43  
44      private final static String MESG_SIZE = "MESG_SIZE"; // The size of the message
45  
46      private boolean checkValidSenderDomain = false;
47      
48      private boolean checkAuthClients = false;
49      
50      private DNSServer dnsServer = null;
51      
52      /***
53       * @see org.apache.avalon.framework.configuration.Configurable#configure(Configuration)
54       */
55      public void configure(Configuration handlerConfiguration) throws ConfigurationException {
56          Configuration configuration = handlerConfiguration.getChild("checkValidSenderDomain",false);
57          if(configuration != null) {
58             checkValidSenderDomain = configuration.getValueAsBoolean();
59             if (checkValidSenderDomain && dnsServer == null) {
60                 throw new ConfigurationException("checkValidSenderDomain enabled but no DNSServer service provided to SMTPServer");
61             }
62          }
63          
64          Configuration configRelay = handlerConfiguration.getChild("checkAuthClients",false);
65          if(configRelay != null) {
66              checkAuthClients = configRelay.getValueAsBoolean();
67          }
68      }
69      
70      /***
71       * @see org.apache.avalon.framework.service.Serviceable#service(ServiceManager)
72       */
73      public void service(ServiceManager serviceMan) throws ServiceException {
74          dnsServer = (DNSServer) serviceMan.lookup(DNSServer.ROLE);
75      }
76      
77      /***
78       * handles MAIL command
79       *
80       * @see org.apache.james.smtpserver.CommandHandler#onCommand(SMTPSession)
81       */
82      public void onCommand(SMTPSession session) {
83          doMAIL(session, session.getCommandArgument());
84      }
85  
86  
87      /***
88       * Handler method called upon receipt of a MAIL command.
89       * Sets up handler to deliver mail as the stated sender.
90       *
91       * @param session SMTP session object
92       * @param argument the argument passed in with the command by the SMTP client
93       */
94      private void doMAIL(SMTPSession session, String argument) {
95          String responseString = null;
96          StringBuffer responseBuffer = session.getResponseBuffer();
97          String sender = null;
98          boolean badSenderDomain = false;
99          
100         if ((argument != null) && (argument.indexOf(":") > 0)) {
101             int colonIndex = argument.indexOf(":");
102             sender = argument.substring(colonIndex + 1);
103             argument = argument.substring(0, colonIndex);
104         }
105         if (session.getState().containsKey(SMTPSession.SENDER)) {
106             responseString = "503 "+DSNStatus.getStatus(DSNStatus.PERMANENT,DSNStatus.DELIVERY_OTHER)+" Sender already specified";
107             session.writeResponse(responseString);
108         } else if (!session.getState().containsKey(SMTPSession.CURRENT_HELO_MODE) && session.useHeloEhloEnforcement()) {
109             responseString = "503 "+DSNStatus.getStatus(DSNStatus.PERMANENT,DSNStatus.DELIVERY_OTHER)+" Need HELO or EHLO before MAIL";
110             session.writeResponse(responseString);
111         } else if (argument == null || !argument.toUpperCase(Locale.US).equals("FROM")
112                    || sender == null) {
113             responseString = "501 "+DSNStatus.getStatus(DSNStatus.PERMANENT,DSNStatus.DELIVERY_INVALID_ARG)+" Usage: MAIL FROM:<sender>";
114             session.writeResponse(responseString);
115         } else {
116             sender = sender.trim();
117             // the next gt after the first lt ... AUTH may add more <>
118             int lastChar = sender.indexOf('>', sender.indexOf('<'));
119             // Check to see if any options are present and, if so, whether they are correctly formatted
120             // (separated from the closing angle bracket by a ' ').
121             if ((lastChar > 0) && (sender.length() > lastChar + 2) && (sender.charAt(lastChar + 1) == ' ')) {
122                 String mailOptionString = sender.substring(lastChar + 2);
123 
124                 // Remove the options from the sender
125                 sender = sender.substring(0, lastChar + 1);
126 
127                 StringTokenizer optionTokenizer = new StringTokenizer(mailOptionString, " ");
128                 while (optionTokenizer.hasMoreElements()) {
129                     String mailOption = optionTokenizer.nextToken();
130                     int equalIndex = mailOption.indexOf('=');
131                     String mailOptionName = mailOption;
132                     String mailOptionValue = "";
133                     if (equalIndex > 0) {
134                         mailOptionName = mailOption.substring(0, equalIndex).toUpperCase(Locale.US);
135                         mailOptionValue = mailOption.substring(equalIndex + 1);
136                     }
137 
138                     // Handle the SIZE extension keyword
139 
140                     if (mailOptionName.startsWith(MAIL_OPTION_SIZE)) {
141                         if (!(doMailSize(session, mailOptionValue, sender))) {
142                             return;
143                         }
144                     } else {
145                         // Unexpected option attached to the Mail command
146                         if (getLogger().isDebugEnabled()) {
147                             StringBuffer debugBuffer =
148                                 new StringBuffer(128)
149                                     .append("MAIL command had unrecognized/unexpected option ")
150                                     .append(mailOptionName)
151                                     .append(" with value ")
152                                     .append(mailOptionValue);
153                             getLogger().debug(debugBuffer.toString());
154                         }
155                     }
156                 }
157             }
158             if (!sender.startsWith("<") || !sender.endsWith(">")) {
159                 responseString = "501 "+DSNStatus.getStatus(DSNStatus.PERMANENT,DSNStatus.ADDRESS_SYNTAX_SENDER)+" Syntax error in MAIL command";
160                 session.writeResponse(responseString);
161                 if (getLogger().isErrorEnabled()) {
162                     StringBuffer errorBuffer =
163                         new StringBuffer(128)
164                             .append("Error parsing sender address: ")
165                             .append(sender)
166                             .append(": did not start and end with < >");
167                     getLogger().error(errorBuffer.toString());
168                 }
169                 return;
170             }
171             MailAddress senderAddress = null;
172             //Remove < and >
173             sender = sender.substring(1, sender.length() - 1);
174             if (sender.length() == 0) {
175                 //This is the <> case.  Let senderAddress == null
176             } else {
177                  
178                 if (sender.indexOf("@") < 0) {
179                     sender = sender + "@localhost";
180                 }
181                 
182                 try {
183                     senderAddress = new MailAddress(sender);
184                 } catch (Exception pe) {
185                     responseString = "501 "+DSNStatus.getStatus(DSNStatus.PERMANENT,DSNStatus.ADDRESS_SYNTAX_SENDER)+" Syntax error in sender address";
186                     session.writeResponse(responseString);
187                     if (getLogger().isErrorEnabled()) {
188                         StringBuffer errorBuffer =
189                             new StringBuffer(256)
190                                     .append("Error parsing sender address: ")
191                                     .append(sender)
192                                     .append(": ")
193                                     .append(pe.getMessage());
194                         getLogger().error(errorBuffer.toString());
195                     }
196                     return;
197                 }
198             }
199             
200             // check only if senderAddress is not null
201             if (checkValidSenderDomain == true && senderAddress != null) {
202                 
203                 /***
204                  * don't check if the ip address is allowed to relay. Only check if it is set in the config. 
205                  */
206                 if (checkAuthClients || !session.isRelayingAllowed()) {
207      
208                     // Maybe we should build a static method in org.apache.james.dnsserver.DNSServer ?
209                     Collection records;
210                 
211                     records = dnsServer.findMXRecords(senderAddress.getHost());
212                     if (records == null || records.size() == 0) {
213                         badSenderDomain = true;
214                     }
215                 
216                     // try to resolv the provided domain in the senderaddress. If it can not resolved do not accept it.
217                     if (badSenderDomain) {
218                         responseString = "501 "+DSNStatus.getStatus(DSNStatus.PERMANENT,DSNStatus.ADDRESS_SYNTAX_SENDER)+ " sender " + senderAddress + " contains a domain with no valid MX records";
219                         session.writeResponse(responseString);
220                         getLogger().info(responseString);
221                     }
222                 }
223             }
224             
225             if (!badSenderDomain) {
226                 session.getState().put(SMTPSession.SENDER, senderAddress);
227                 responseBuffer.append("250 "+DSNStatus.getStatus(DSNStatus.SUCCESS,DSNStatus.ADDRESS_OTHER)+" Sender <")
228                               .append(sender)
229                               .append("> OK");
230                 responseString = session.clearResponseBuffer();
231                 session.writeResponse(responseString);
232             }
233         }
234     }
235 
236     /***
237      * Handles the SIZE MAIL option.
238      *
239      * @param session SMTP session object
240      * @param mailOptionValue the option string passed in with the SIZE option
241      * @param tempSender the sender specified in this mail command (for logging purpose)
242      * @return true if further options should be processed, false otherwise
243      */
244     private boolean doMailSize(SMTPSession session, String mailOptionValue, String tempSender) {
245         int size = 0;
246         try {
247             size = Integer.parseInt(mailOptionValue);
248         } catch (NumberFormatException pe) {
249             // This is a malformed option value.  We return an error
250             String responseString = "501 "+DSNStatus.getStatus(DSNStatus.PERMANENT,DSNStatus.DELIVERY_INVALID_ARG)+" Syntactically incorrect value for SIZE parameter";
251             session.writeResponse(responseString);
252             getLogger().error("Rejected syntactically incorrect value for SIZE parameter.");
253             return false;
254         }
255         if (getLogger().isDebugEnabled()) {
256             StringBuffer debugBuffer =
257                 new StringBuffer(128)
258                     .append("MAIL command option SIZE received with value ")
259                     .append(size)
260                     .append(".");
261                     getLogger().debug(debugBuffer.toString());
262         }
263         long maxMessageSize = session.getConfigurationData().getMaxMessageSize();
264         if ((maxMessageSize > 0) && (size > maxMessageSize)) {
265             // Let the client know that the size limit has been hit.
266             String responseString = "552 "+DSNStatus.getStatus(DSNStatus.PERMANENT,DSNStatus.SYSTEM_MSG_TOO_BIG)+" Message size exceeds fixed maximum message size";
267             session.writeResponse(responseString);
268             StringBuffer errorBuffer =
269                 new StringBuffer(256)
270                     .append("Rejected message from ")
271                     .append(tempSender != null ? tempSender : null)
272                     .append(" from host ")
273                     .append(session.getRemoteHost())
274                     .append(" (")
275                     .append(session.getRemoteIPAddress())
276                     .append(") of size ")
277                     .append(size)
278                     .append(" exceeding system maximum message size of ")
279                     .append(maxMessageSize)
280                     .append("based on SIZE option.");
281             getLogger().error(errorBuffer.toString());
282             return false;
283         } else {
284             // put the message size in the message state so it can be used
285             // later to restrict messages for user quotas, etc.
286             session.getState().put(MESG_SIZE, new Integer(size));
287         }
288         return true;
289     }
290 
291 
292 }