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