View Javadoc

1   package org.paneris.messageboard.receivemail;
2   
3   import java.io.BufferedInputStream;
4   import java.io.BufferedReader;
5   import java.io.IOException;
6   import java.io.InputStreamReader;
7   import java.io.OutputStreamWriter;
8   import java.io.PrintWriter;
9   import java.io.PushbackInputStream;
10  import java.net.Socket;
11  import java.sql.SQLException;
12  import java.util.Properties;
13  
14  import javax.mail.MessagingException;
15  import javax.mail.internet.InternetAddress;
16  
17  import org.paneris.jal.model.DBConnectionManager;
18  import org.paneris.jal.model.Log;
19  import org.paneris.util.ExceptionUtils;
20  import org.paneris.util.StringUtils;
21  
22  /**
23   * An SMTP session for receiving one or more incoming emails from a
24   * client (in fact typically <TT>sendmail</TT>).
25   *
26   * One of these threads is spawned each time <TT>sendmail</TT>
27   * connects to our server socket to forward some message(s) to us.
28   */
29  
30  class SMTPSession extends Thread {
31  
32    private String smtpIdentifier;
33    private Socket withClient;
34    private PushbackInputStream fromClientPushBack;
35    private BufferedReader fromClient;
36    private DBConnectionManager connMgr;
37    private Properties databaseNameOfDomain;
38    private int bufSize;
39    private PrintWriter toClient;
40    private Log log;
41  
42    private String sender = null;
43    private MessageBoardStore store = null;
44    private MessageBoardStore.SenderID senderID = null;
45    private MessageBoardStore.RecipientID recipientID = null;
46  
47    /**
48     * A handler for a session with <TT>sendmail</TT>
49     *
50     * @param smtpIdentifier      what we should report our hostname as:
51     *                            this must be different from what
52     *                            <TT>sendmail</TT> thinks it is called,
53     *                            or it will fail with a loopback error
54     *
55     * @param withClient          the socket on which <TT>sendmail</TT>
56     *                            has just connected
57     *
58     * @param connMgr             our handle to the DBMS
59     *
60     * @param databaseNameOfDomain the mapping of each mail domain we handle
61     *                            to the database in which the
62     *                            corresponding messageboard lives (see
63     *                            <TT>smtpServer.properties.example</TT>)
64     *
65     * @param bufSize             how much to buffer the IPC stream 
66     *                            coming from <TT>sendmail</TT>
67     *                            (non-critical, say 64k)
68     *
69     * @param log                 where to report errors
70     */
71  
72    SMTPSession(String smtpIdentifier, Socket withClient,
73                DBConnectionManager connMgr, Properties databaseNameOfDomain,
74                int bufSize, Log log)
75        throws IOException {
76      this.smtpIdentifier = smtpIdentifier;
77      this.withClient = withClient;
78      this.connMgr = connMgr;
79      this.databaseNameOfDomain = databaseNameOfDomain;
80      this.bufSize = bufSize;
81      bufSize = this.bufSize;
82      this.log = log;
83  
84      fromClientPushBack =
85        new PushbackInputStream(new BufferedInputStream(
86                                      withClient.getInputStream(),
87                                      bufSize));
88  
89      fromClient = new BufferedReader(new InputStreamReader(fromClientPushBack));
90  
91      toClient =
92        new PrintWriter(new OutputStreamWriter(withClient.getOutputStream(),
93                 "8859_1"),
94            true);
95    }
96  
97    /**
98     * Handle an SMTP <TT>RSET</TT> command, or generally otherwise tidy up.
99     */
100 
101   private void reset() {
102     sender = null;
103     senderID = null;
104     recipientID = null;
105     if (store != null) {
106       try {
107         store.close(); 
108       } catch (Exception e) {
109         ; // Ignore closing errors
110       }
111       store = null;
112     }
113   }
114 
115   /**
116    * Handle an SMTP <TT>MAIL FROM:</TT> command.
117    *
118    * @param address    the address after the colon
119    */
120   private void mailFrom(String address) {
121     if (address.charAt(0) == '<') {
122       // hmm, not sure this actually happens
123       address = address.substring(1, address.length() - 1);
124     }
125 
126     // at this stage we don't have a database name so we can't verify
127 
128     sender = address;
129     toClient.println("250 " + address + "... Sender provisionally OK");
130   }
131 
132   /**
133    * The ``message store'' (wrapper for messageboard database tables)
134    * associated with a given email address.
135    *
136    * @param address    the email address in question; the domain
137    *                            part after the <TT>@</TT> is looked up in
138    *                            the domain-to-database properties map
139    */
140 
141   private MessageBoardStore storeForAddress(String address)
142       throws MessagingException, IOException {
143     int atIndex = address.indexOf('@');
144     if (atIndex == -1)
145       throw new MessagingException("`" + address + "': missing domain, " +
146            "so can't determine which database to use");
147 
148     String domain = address.substring(atIndex + 1);
149     String propertyName =
150         "org.paneris.messageboard.receivemail.database." + domain;
151 
152     String databaseName = databaseNameOfDomain.getProperty(propertyName);
153 
154     if (databaseName == null)
155       throw new MessagingException(
156           "`" + domain + "' is not a messageboard mail domain " +
157           "(no entry `" + propertyName + "' in properties)");
158 
159     try {
160       return new MessageBoardStore(connMgr, databaseName, log);
161     }
162     catch (IOException e) {
163       throw new IOException(
164           "failed to open message store `" + domain + "' -> `" + databaseName +
165     "': " + e);
166     }
167   }
168 
169   /**
170    * Handle an SMTP <TT>RCPT TO:</TT> command
171    *
172    * @param address    the address after the colon
173    */
174 
175   private void rcptTo(String address) throws Exception {
176     if (sender == null)
177       toClient.println("503 Need MAIL before RCPT");
178 
179     else if (store != null) {
180       // FIXME actually the logic of this could be worked out, but for now ...
181 
182       toClient.println("553 a message can only appear on one board, " +
183            "but this one was copied to several");
184     }
185 
186     else {
187       if (address.charAt(0) == '<') {
188         // hmm, not sure this actually happens
189         address = address.substring(1, address.length() - 1);
190       }
191       try {
192         store = storeForAddress(address);
193 
194         // now we know which database we are concerned with, 
195         // we can validate the sender
196 
197         senderID = store.senderIDOfAddress(new InternetAddress(sender));
198         recipientID =
199             store.recipientIDOfAddress(new InternetAddress(address));
200 
201         toClient.println("250 Recipient OK");
202       }
203       catch (MessagingException e) {
204         toClient.println("550 " + // RFC 821: "not found"
205         StringUtils.tr(e.getMessage(), "\n\r", "  "));
206         log.warning("board address `" + address + "' rejected: " + e);
207       }
208     }
209   }
210 
211   /**
212    * Handle an SMTP <TT>DATA</TT> command---post a message.
213    */
214 
215   private void data() throws Exception {
216     if (senderID == null || recipientID == null)
217       toClient.println("503 Need MAIL command");
218     else {
219       toClient.println("354 Enter mail, end with \".\" on a line by itself");
220 
221       try {
222         Object messageID = store.messageAccept(senderID, recipientID,
223                 new DotTerminatedInputStream(fromClientPushBack));
224 
225         toClient.println("250 "+messageID+" Message accepted for delivery");
226       }
227       catch (SQLException e) {
228         if (e.getMessage().startsWith("SQL Statement too long")) {
229           toClient.println("552 Your message message is too long---" +
230                            "can you split it up?");
231           reset();
232         }
233         else { throw e; }
234       }
235 
236       reset();
237     }
238   }
239 
240   /**
241    * Do the business.
242    */
243   public void run () {
244     try {
245       toClient.println("220 " + smtpIdentifier + " SMTP");
246       for (;;) {
247         String command = fromClient.readLine().trim();
248 
249         if (command.regionMatches(true, 0, "HELO", 0, 4))
250           toClient.println("250 " + smtpIdentifier);
251 
252         else if (command.regionMatches(true, 0, "MAIL FROM:", 0, 10))
253           mailFrom(command.substring(10).trim());
254 
255         else if (command.regionMatches(true, 0, "RCPT TO:", 0, 8))
256           rcptTo(command.substring(8).trim());
257 
258         else if (command.regionMatches(true, 0, "DATA", 0, 4))
259           data();
260 
261         else if (command.regionMatches(true, 0, "RSET", 0, 4)) {
262           reset();
263           toClient.println("250 Reset state");
264         }
265 
266         else if (command.regionMatches(true, 0, "QUIT", 0, 4)) {
267           toClient.println("221 " + smtpIdentifier + " closing connection");
268           break;
269         }
270         // does it matter that we don't do VRFY?
271         else
272           toClient.println("500 Command unrecognized: \"" + command + "\"");
273       }
274     }
275     catch (Exception e) {
276       toClient.println("554 Sorry: something is wrong with this server---" +
277                        StringUtils.tr(e.toString(), "\n\r", "  "));
278       log.error("post of message from `" + sender + "' failed:\n" +
279                 ExceptionUtils.stackTrace(e));
280     }
281     finally {
282       try {
283         reset();
284         withClient.close();
285       }
286       catch (Exception e) {
287         ; // Ignore closing errors
288       }
289     }
290   }
291 }