View Javadoc

1   package org.paneris.messageboard.receivemail;
2   
3   import java.io.File;
4   import java.io.IOException;
5   import java.io.InputStream;
6   import java.sql.Connection;
7   import java.sql.ResultSet;
8   import java.sql.SQLException;
9   import java.sql.Statement;
10  
11  import javax.mail.MessagingException;
12  import javax.mail.Multipart;
13  import javax.mail.Part;
14  import javax.mail.Session;
15  import javax.mail.internet.InternetAddress;
16  import javax.mail.internet.MimeMessage;
17  
18  import org.paneris.jal.model.DBConnectionManager;
19  import org.paneris.jal.model.DDField;
20  import org.paneris.jal.model.DDRecord;
21  import org.paneris.jal.model.Log;
22  import org.paneris.messageboard.model.Attachment;
23  import org.paneris.messageboard.model.Message;
24  import org.paneris.util.ExceptionUtils;
25  import org.paneris.util.IoUtils;
26  import org.paneris.util.SQLUtils;
27  
28  /**
29   * A wrapper to make a handy interface to the database tables comprising
30   * a set of messageboards
31   */
32  
33  class MessageBoardStore {
34    private String database;
35    private DBConnectionManager connMgr;
36    private Connection conn;
37    private Log log;
38  
39    /**
40     * A wrapper for the messageboards in a given database
41     *
42     * @param connMgr            our handle to the DBMS
43     * @param database           the name of the database
44     * @param log                where to log trouble
45     */
46  
47    public MessageBoardStore(DBConnectionManager connMgr, String database,
48                             Log log)
49        throws IOException {
50      this.connMgr = connMgr;
51      conn = connMgr.getConnection("ReceiveMail", database);
52      if (conn == null) throw new RuntimeException("DB connection for " + 
53                           database + " is null.");
54      this.database = database;
55      this.log = log;
56    }
57  
58    /**
59     * Tidy up
60     */
61  
62    public void close() {
63      connMgr.freeConnection(database, conn);
64    }
65  
66    protected void finalize() {
67      close();
68    }
69  
70    class SenderID {
71      protected String id;
72    }
73  
74    /**
75     * Return the user (poster) ID with a given email address
76     *
77     * @param email            their <TT>x@y.z</TT> address, case-insensitive
78     *
79     * @throws MessagingException e.g. if the user doesn't exist
80     * @throws SQLException       if something fails
81     *
82     * @return                   any object which will be recognised later by
83     *                            <TT>messageAccept</TT>
84     *
85     * @see #messageAccept
86     */
87  
88    public SenderID senderIDOfAddress(InternetAddress email)
89        throws MessagingException, SQLException, Exception {
90      Statement s = conn.createStatement();
91      String from = email.getAddress();
92      try {
93        String upper = connMgr.sqlUppercaseOperator(database);
94        ResultSet rs =
95          s.executeQuery("SELECT id FROM users " +
96                         "WHERE TRIM(" + upper + "(email)) = " +
97                         "'" + SQLUtils.escapedString(from.toUpperCase()) + "'");
98        if (rs.next()) {
99          SenderID id = new SenderID();
100         id.id = rs.getString(1);
101         return id;
102       }
103       else
104         throw new MessagingException(
105             "user `" + email + "' " +
106             "not authorised to post on this message board");
107     }
108     finally {
109       s.close();
110     }
111   }
112 
113   class RecipientID {
114     Integer boardID;
115     Integer parentID;
116   }
117 
118   /**
119    * Return the message board (and parent message) ID with a given email
120    * address
121    *
122    * @param email            has the form
123    *                            <I>[parentID.]boardName@domain</I>;
124    *                            <I>domain</I> is ignored, and what is returned
125    *                            identifies both the board and the parent message
126    *
127    * @throws MessagingException e.g. if the board doesn't exist
128    * @throws SQLException       if something fails
129    *
130    * @return                   any object which will be recognised later by
131    *                            <TT>messageAccept</TT>
132    *
133    * @see #messageAccept
134    */
135 
136   public RecipientID recipientIDOfAddress(InternetAddress email)
137       throws MessagingException, SQLException, Exception {
138     try {
139       String to = email.getAddress();
140 
141       RecipientID recipientID = new RecipientID();
142       String boardName;
143 
144       int atIndex = to.indexOf('@');
145       if (atIndex == -1) atIndex = to.length() - 1;
146       int dotIndex = to.indexOf('.');
147       if (dotIndex > 0 && dotIndex < atIndex) {
148         boardName = to.substring(dotIndex + 1, atIndex);
149         try {
150           recipientID.parentID = Integer.valueOf(to.substring(0, dotIndex));
151         }
152         catch (NumberFormatException e) {
153           throw new MessagingException("illegal parent message ID `" +
154                                        to.substring(0, dotIndex) + "'");
155         }
156       }
157       else {
158         boardName = to.substring(0, atIndex);
159         recipientID.parentID = new Integer(0);
160       }
161 
162       Statement s = conn.createStatement();
163       try {
164         String upper = connMgr.sqlUppercaseOperator(database);
165         ResultSet rs =
166           s.executeQuery("SELECT id FROM messageboards " +
167                          "WHERE TRIM(" + upper + "(name)) = " +
168                          "'" + SQLUtils.escapedString(boardName.toUpperCase()) +
169                          "'");
170         if (rs.next())
171           recipientID.boardID = new Integer(rs.getInt(1));
172         else
173           throw new MessagingException("unknown messageboard `" +
174                                        boardName + "'");
175       }
176       finally {
177         s.close();
178       }
179 
180       return recipientID;
181     }
182     catch (SQLException e) {
183       throw new MessagingException("SQL error " + e);
184     }
185   }
186 
187   /**
188    * Write an attachment into the attachment table and filesystem
189    */
190 
191   private void attachmentWrite(
192       String database, Connection conn, Integer boardId, Integer id,
193       String fileName, String contentType, byte[] content)
194           throws Exception {
195 
196     Attachment att = new Attachment(database);
197     ((DDField)att.get("message")).setValue(id);
198     att.setFieldValue("filename", fileName);
199     att.setBoard(boardId);
200     att.setData(content);
201 
202     // sort out the MIME type
203 
204     int semicolonIndex = contentType.indexOf(';');
205     if (semicolonIndex != -1)
206       contentType = contentType.substring(0, semicolonIndex);
207 
208     Statement s = conn.createStatement();
209     ResultSet rs = s.executeQuery(
210         "SELECT id FROM attachmenttypes WHERE type = '" +
211         SQLUtils.escapedString(contentType) + "'");
212     Integer ct;
213     if (rs.next()) {
214       ct = new Integer(rs.getInt(1));
215     } else {
216       DDRecord attachmentType = new DDRecord(database,"attachmenttypes");
217       attachmentType.setFieldValue("type", contentType);
218       attachmentType.write();
219       DDField atid = (DDField)attachmentType.get("id");
220       ct = (Integer)atid.getValue();
221     }
222     ((DDField)att.get("type")).setValue(ct);
223     s.close();
224 
225     att.write();
226 
227     if (contentType.startsWith("application/msword")) {
228       byte[] htmlContent = null;
229       try {
230     String attPath = att.getAttachmentPath();
231     String imgSubDir = att.getFieldValue("id") + "-wvHtmlIMG";
232 
233     String imgPath = attPath + "/" + imgSubDir;
234     if (!new File(imgPath).mkdir())
235       throw new IOException("can't mkdir " + imgPath);
236 
237         htmlContent = WordToHTML.translated(content, attPath, imgSubDir);
238       }
239       catch (Exception e) {
240     e.printStackTrace();
241         log.warning("Translating Word document: " + e.getMessage());
242       }
243       catch (Error e) {
244     e.printStackTrace();
245     throw e;
246       }
247 
248       if (htmlContent != null)
249     attachmentWrite(database, conn, boardId, id,
250             fileName == null ? "anon.html" : fileName + ".html",
251             "text/html", htmlContent);
252     }
253   }
254 
255   private Object getContent(MimeMessage message, Part part)
256       throws MessagingException, IOException {
257     if (part.getContentType().startsWith("text/plain")) {
258       String[] mailer = message.getHeader("X-Mailer");
259       if (mailer != null && mailer.length > 0 &&
260           mailer[0].startsWith("Mozilla 4.6"))
261         part.setHeader("Content-Transfer-Encoding", "8bit");
262     }
263 
264     return part.getContent();
265   }
266 
267 
268   /**
269    * Stick a message into the messageboards
270    *
271    * @param senderID            obtained from <TT>senderIDOfAddress</TT>
272    * @param recipientIDs      obtained from <TT>recipientIDOfAddress</TT>
273    * @param text            the raw (un-decoded) data of the message
274    *
275    * @throws Exception            because the routines it call do ...
276    *
277    * @return                   a message ID for the message
278    */
279 
280   public Object messageAccept(SenderID senderID, RecipientID recipientID,
281                               InputStream text) throws Exception {
282     MimeMessage message = new MimeMessage(
283       Session.getDefaultInstance(System.getProperties(), null),
284       text);
285 
286     // write the record straight away in order to get an id number for the
287     // attachments (if any)
288 
289     Message m = new Message(database);
290     m.write();
291     DDField idf = (DDField)m.get("id");
292     Integer id = (Integer)idf.getValue();
293 
294     // do the easy bits
295 
296     m.setFieldValue("subject",
297                     message.getSubject() == null ?
298                       "(no subject)" :
299                       message.getSubject());
300 
301     ((DDField)m.get("board")).setValue(recipientID.boardID);
302     ((DDField)m.get("parent")).setValue(recipientID.parentID);
303     ((DDField)m.get("author")).setValue(senderID.id);
304 
305     // process attachments
306 
307     StringBuffer bodyText = new StringBuffer(100);
308 
309     Object content = getContent(message, message);
310     if (content instanceof String) {
311       // it's text
312       bodyText.append((String)content);
313     }
314     else if (content instanceof Multipart) {
315       Multipart parts = (Multipart)content;
316       for (int p = 0; p < parts.getCount(); ++p) {
317         Part part = parts.getBodyPart(p);
318 
319         // can it go in line?
320 
321         Object partContent = getContent(message, part);
322 
323         if (partContent instanceof String &&
324             part.getFileName() == null)
325           bodyText.append((String)partContent);
326         else
327           attachmentWrite(database, conn, recipientID.boardID, id,
328                           part.getFileName(), part.getContentType(),
329                           IoUtils.slurp(part.getInputStream(), 1024));
330       }
331     }
332     else {
333       // message is one, non-text item
334 
335       attachmentWrite(database, conn, recipientID.boardID, id,
336                       null, message.getContentType(),
337                       IoUtils.slurp(message.getInputStream(), 100));
338     }
339 
340     // write the message down and email it out
341     // put a 7k limit on message size
342     String messageText = bodyText.toString();
343     if (messageText.length() > 7168) {
344       attachmentWrite(database, conn, recipientID.boardID, id,
345                       "", "truncation", messageText.getBytes());
346       messageText = messageText.substring(0,7168);
347       messageText += "\n\nThis message has been truncated, the complete message has been stored as an attachment.";
348     }
349     m.setFieldValue("message", messageText);
350     m.write();
351     m.setAttachments();
352     (new DistributeThread(m, log)).start();
353 
354     return id;
355   }
356 }
357 
358 /**
359  * A daemon to redistribute emails to the messageboard's distribution list
360  */
361 
362 class DistributeThread extends Thread {
363   private Message m;
364   private Log log;
365 
366   public DistributeThread(Message m, Log log) {
367     this.m = m;
368     this.log = log;
369   }
370 
371   public void run() {
372     try {
373       m.distribute();
374     }
375     catch (Exception e) {
376       log.error("Redistributing incoming email: " +
377                 ExceptionUtils.stackTrace(e));
378     }
379   }
380 }