View Javadoc

1   /*
2    * Copyright (c) 2004-2007 Creative Sphere Limited.
3    * All rights reserved. This program and the accompanying materials
4    * are made available under the terms of the Eclipse Public License v1.0
5    * which accompanies this distribution, and is available at
6    * http://www.eclipse.org/legal/epl-v10.html
7    *
8    * Contributors:
9    *
10   *   Creative Sphere - initial API and implementation
11   *
12   */
13  package org.abstracthorizon.mercury.smtp.command;
14  
15  import java.io.BufferedReader;
16  import java.io.File;
17  import java.io.IOException;
18  import java.io.InputStream;
19  import java.io.InputStreamReader;
20  import java.io.OutputStream;
21  import java.net.InetSocketAddress;
22  import java.net.Socket;
23  import java.net.SocketTimeoutException;
24  import java.text.SimpleDateFormat;
25  import java.util.ArrayList;
26  import java.util.Date;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Properties;
30  
31  import javax.mail.Folder;
32  import javax.mail.Message;
33  import javax.mail.MessagingException;
34  import javax.mail.Session;
35  import javax.mail.internet.MimeMessage;
36  
37  import org.abstracthorizon.mercury.common.command.CommandException;
38  import org.abstracthorizon.mercury.common.io.TempStorage;
39  import org.abstracthorizon.mercury.smtp.SMTPResponses;
40  import org.abstracthorizon.mercury.smtp.SMTPScanner;
41  import org.abstracthorizon.mercury.smtp.SMTPSession;
42  import org.abstracthorizon.mercury.smtp.exception.ParserException;
43  import org.abstracthorizon.mercury.smtp.util.Path;
44  import org.slf4j.Logger;
45  import org.slf4j.LoggerFactory;
46  
47  /**
48   * SMTP DATA command.
49   *
50   * @author Daniel Sendula
51   */
52  public class DataCommand extends SMTPCommand {
53  
54      protected static final byte[] CRLF = new byte[] { '\r', '\n' };
55  
56      public static final SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z");
57  
58      protected static final Logger logger = LoggerFactory.getLogger(DataCommand.class);
59  
60      public static Session mailSession = Session.getDefaultInstance(new Properties()); // Do this more gracefully
61  
62  
63      /**
64       * Constructor
65       */
66      public DataCommand() {
67      }
68  
69      /**
70       * Executed the command
71       * @param connection smtp session
72       * @throws CommandException
73       * @throws IOException
74       * @throws ParserException
75       */
76      protected void execute(SMTPSession connection) throws CommandException, IOException, ParserException {
77          if (connection.getState() != SMTPSession.STATE_MAIL) {
78              connection.sendResponse(SMTPResponses.BAD_SEQUENCE_OF_COMMANDS_RESPONSE);
79              return;
80          }
81          try {
82              SMTPScanner scanner = connection.getScanner();
83  
84              readExtraParameters(connection, scanner);
85  
86              if (precheck(connection)) {
87                  TempStorage tempStorage = new TempStorage();
88                  OutputStream out = tempStorage.getOutputStream();
89                  out.write(composeReceivedHeader(connection).getBytes());
90                  out.write(CRLF);
91                  try {
92                      connection.setStreamDebug(false);
93                      readMail(connection.getInputStream(), out);
94                      out.close();
95                  } catch (IOException e) {
96                      // TODO should we drop the line here?
97                      // Scenario: data is late in the middle of e-mail
98                      // we have timeout + data arrive
99                      // -> loads of syntax errors and other side gives up
100                     // Solution: soon we have IO exception - we send response
101                     // and close the socket?
102                     if (!(e instanceof SocketTimeoutException)) {
103                         logger.error("Problem reading message", e);
104                     }
105                     connection.sendResponse(SMTPResponses.GENERIC_ERROR_RESPONSE);
106                     connection.getMailSessionData().addToTotalBytes(tempStorage.getSize());
107                     tempStorage.clear();
108                     tempStorage = null;
109                 } finally {
110                     connection.setStreamDebug(true);
111                 }
112 
113                 // prevents further processing in case of an error while reading
114                 // message from the input
115                 if (tempStorage != null) {
116                     try {
117                         MimeMessage message = new MimeMessage(mailSession, tempStorage.getInputStream());
118                         File file = tempStorage.getFile();
119                         if (file != null) {
120                             message.setFileName(file.getAbsolutePath());
121                         }
122                         connection.getMailSessionData().setMessage(message);
123                         processMail(connection, message);
124                     } catch (MessagingException e) {
125                         logger.error("Problem creating message", e);
126                     } finally {
127                         connection.getMailSessionData().setMessage(null);
128                         connection.getMailSessionData().addToTotalBytes(tempStorage.getSize());
129                         tempStorage.clear();
130                     }
131                 }
132             }
133         } finally {
134             connection.setState(SMTPSession.STATE_READY);
135         }
136     }
137 
138     /**
139      * Adds "Received:" header.
140      * @param connection SMTP session
141      * @throws IOException io exception
142      */
143     protected String composeReceivedHeader(SMTPSession connection) throws IOException {
144         Date now = new Date();
145         StringBuffer received = new StringBuffer("Received:");
146         Socket socket = (Socket)connection.adapt(Socket.class);
147         if (socket != null) {
148             append(received, " from " + connection.getMailSessionData().getSourceDomain() + getTCPInfo(socket));
149         } else {
150             append(received, " from " + connection.getMailSessionData().getSourceDomain());
151         }
152         append(received, " by " + connection.getConnectionHandler().getStorageManager().getMainDomain());
153         append(received, " for " + composeDestMailboxes(connection.getMailSessionData().getDestinationMailboxes()));
154         append(received, "; " + format.format(now));
155         return received.toString();
156     }
157 
158     /**
159      * Returns inet address as string
160      * @param socket socket
161      * @return string representation of a string
162      */
163     protected String getTCPInfo(Socket socket) {
164         return " (["+((InetSocketAddress)socket.getRemoteSocketAddress()).getAddress().getHostAddress()+"])";
165     }
166 
167     /**
168      * Composes Received header's list of destination mailboxes
169      * @param dest list of mailboxes
170      * @return string representation of 's list of destination mailboxes
171      */
172     protected String composeDestMailboxes(List<Path> dest) {
173         StringBuffer buf = new StringBuffer();
174         int i = 0;
175         while ((i < dest.size()) && (i < 3)) {
176             Path path = dest.get(i);
177             if (i > 0) {
178                 buf.append(',');
179             }
180             buf.append(path.getMailbox());
181             i = i + 1;
182         }
183         if (i < dest.size()) {
184             buf.append(",...");
185         }
186         return buf.toString();
187     }
188 
189     /**
190      * Appends elements to header value making it sure it is not over 999 chars.
191      * @param buf buffer to append to
192      * @param part new elemnt to added to header value
193      */
194     protected void append(StringBuffer buf, String part) {
195         // TODO this is not optimised. No reason for lastIndexOf to be called that many times
196         int i = buf.lastIndexOf("\r\n");
197         if (i < 0) {
198             i = 0;
199         } else {
200             i = i+2;
201         }
202         if (buf.length()-i+part.length() > 999) {
203             buf.append("\r\n  ");
204         }
205         buf.append(part);
206     }
207 
208     /**
209      * Reads raw mail from the input stream. <br>
210      * Method to be overriden for DATA extensions.
211      *
212      * @param in input stream mail is read from. Usually input stream from the
213      *            socket
214      * @param mail output stream mail is written to
215      * @throws IOException in case of an exception while reading mail
216      */
217     protected void readMail(InputStream in, OutputStream mail) throws IOException {
218 
219         boolean first = true;
220         BufferedReader reader = new BufferedReader(new InputStreamReader(in, "ISO-8859-1"));
221 
222         String line = reader.readLine();
223         while (line != null) {
224             if (first) {
225                 first = false;
226             } else {
227                 if (line.equals(".")) { return; }
228             }
229             if (line.startsWith(".")) {
230                 line = line.substring(1);
231             }
232             mail.write(line.getBytes());
233             mail.write(CRLF);
234             line = reader.readLine();
235         }
236     }
237 
238     /**
239      * Obtains extra parameters. <br>
240      * Method to be overriden for DATA extensions.
241      *
242      * @param session SMTP session
243      * @param scanner STMP scanner
244      * @throws IOException io exception
245      * @throws ParserException parsing exception
246      * @throws CommandException command exception
247      */
248     protected void readExtraParameters(SMTPSession connection, SMTPScanner scanner) throws IOException, ParserException, CommandException {
249         scanner.check_eol();
250     }
251 
252     /**
253      * Returns <code>true</code> in case it is ok with proceeding with the
254      * reading input stream. This method is responsible of sending response back
255      * to the client
256      *
257      * <br>
258      * Method to be overriden for filtering purposes.
259      *
260      * @param session SMTP session
261      * @return <code>true</code> in case it is ok with proceeding with the
262      *         reading input stream.
263      * @throws IOException
264      */
265     protected boolean precheck(SMTPSession connection) throws CommandException, IOException {
266         connection.sendResponse(SMTPResponses.START_DATA_RESPONSE);
267         return true;
268     }
269 
270     /**
271      * Returns <code>true</code>
272      * @param connection smtp session
273      * @return <code>true</code>
274      * @throws IOException
275      */
276     protected boolean postcheck(SMTPSession connection) throws IOException {
277         // Message is going to be delivered - so we can reset inactivity counter
278         connection.resetLastAccessed();
279         return true;
280     }
281 
282     /**
283      * Sets positive response if there are successful mailboxes
284      * @param connection smtp session
285      * @param hasSuccessfuls has successful mailboxes
286      * @throws IOException
287      */
288     protected void postProcessing(SMTPSession connection, boolean hasSuccessfuls) throws IOException {
289         if (hasSuccessfuls) {
290             connection.sendResponse(SMTPResponses.OK_RESPONSE);
291         } else {
292 //          session.sendResponse(SMTPResponses.GENERIC_ERROR_RESPONSE);
293             connection.sendResponse(SMTPResponses.MAILBOX_UNAVAILABLE_RESPONSE);
294         }
295     }
296 
297     /**
298      * Processes mail.
299      *
300      * @param connection SMTP session
301      * @param message mime message
302      * @throws IOException needed for sending responses
303      */
304     protected void processMail(SMTPSession connection, MimeMessage message) throws IOException {
305         if (postcheck(connection)) {
306             boolean hasSuccesfuls = false;
307             List<Path> externals = new ArrayList<Path>();
308             Iterator<Path> it = connection.getMailSessionData().getDestinationMailboxes().iterator();
309             while (it.hasNext()) {
310                 Path path = it.next();
311                 if (path.isLocalMailbox()) {
312                     if (processLocalMailbox(connection, path, message)) {
313                         hasSuccesfuls = true;
314                     }
315                 } else if (!path.isLocalDomain()) {
316                     externals.add(path);
317                 }
318             } // while
319 
320             if (externals.size() > 0) {
321                 processExternalMail(connection, externals, message);
322             }
323 
324             postProcessing(connection, hasSuccesfuls);
325         }
326     }
327 
328     /**
329      * Processes local storage mails.
330      *
331      * @param connection SMTP session
332      * @param path path object
333      * @param message message
334      * @return <code>true</code> in case it succeded
335      */
336     protected boolean processLocalMailbox(SMTPSession connection, Path path, MimeMessage message) {
337         try {
338             Folder folder = path.getFolder();
339             folder.open(Folder.READ_WRITE);
340 
341             if (message.getSentDate() == null) {
342                 Date rd = message.getReceivedDate();
343                 if (rd == null) {
344                     rd = new Date(System.currentTimeMillis());
345                 }
346                 message.setSentDate(rd);
347                 message.setHeader("Date", format.format(rd));
348             }
349 
350             try {
351                 folder.appendMessages(new Message[] { message });
352             } finally {
353                 folder.close(false);
354             }
355             return true;
356         } catch (MessagingException e) {
357             logger.error("Storing problem", e);
358         }
359         return false;
360     }
361 
362     /**
363      * Processes external mails - invokes transport protocol in sending mail
364      * further or caching it localing for delayed send.
365      *
366      * @param connection smtp session
367      * @param externals list of <code>Path</code> objects
368      * @param message mime message to be forwarded
369      * @return <code>true</code> in case it succeded
370      */
371     protected boolean processExternalMail(SMTPSession connection, List<Path> externals, MimeMessage message) {
372         // TODO
373         return false;
374     }
375 
376 }