001    package edu.harvard.deas.hyperenc;
002            
003    import java.io.IOException;
004    import java.util.ArrayList;
005    import java.util.Date;
006    import java.util.List;
007    import java.util.Properties;
008    
009    import javax.mail.AuthenticationFailedException;
010    import javax.mail.Flags;
011    import javax.mail.Folder;
012    import javax.mail.Message;
013    import javax.mail.MessagingException;
014    import javax.mail.NoSuchProviderException;
015    import javax.mail.Session;
016    import javax.mail.Store;
017    import javax.mail.Transport;
018    import javax.mail.Message.RecipientType;
019    import javax.mail.internet.MimeMessage;
020    
021    import org.apache.log4j.Logger;
022    
023    import edu.harvard.deas.hyperenc.util.HexCoder;
024    
025    /**
026     * Sends and receives messages via e-mail. Each EmailHyperCommunicator sends and
027     * receives messages for a particular e-mail account. Clients may request that
028     * messages be sent from that account, and may check for incoming messages to
029     * that account.
030     * <p>
031     * The presence of an e-mail header with the name specified in
032     * <code>HYPERENC_HEADER</code> indicates that the e-mail contains a
033     * hyper-encrypted message. E-mail messages without this header are ignored. The
034     * value of the header indicates the format of the body. The only format
035     * currently supported is "text"; that format is described below. Future
036     * implementations may support XML-based bodies or other formats.
037     * <p>
038     * The address specified in the <code>From:</code> e-mail header corresponds to
039     * the <i>sender</i> field of a HyperMessage. The address specified in the
040     * <code>To:</code> header corresponds to the <i>recipient</i> field of a
041     * HyperMessage. If the e-mail Message has more than one recipient, all but the
042     * first are ignored. These addresses are converted to their corresponding
043     * Contacts by looking up the Address in the ContactList provided at
044     * construction.
045     * 
046     * <h3>Message format</h3>
047     * <p>
048     * The "text" format is a hastily-assembled, somewhat fragile format for
049     * encoding hyper-encryption messages. Future versions will support improved
050     * formats (XML-based, for example).
051     * <p>
052     * An e-mail body with the "text" format has five sections, which may appear in
053     * any order: <i>type</i>, <i>block list</i>, <i>MAC</i>, <i>subject</i>, and
054     * <i>content</i>. (Message produced by this class give the five sections in
055     * that order.) The sections may be separated by any number of newlines; leading
056     * and trailing whitespace in all sections is ignored.
057     * <ul>
058     * <li>
059     * The <i>type</i> section starts with the header string "*TYPE*" on its own
060     * line. The next line contains a token indicating the type of the message. That
061     * token must be a string representation of a HyperMessageType. This corresponds
062     * to the <i>type</i> field of a HyperMessage.</li>
063     * <li>
064     * The <i>block list</i> section starts with the header string "*PADSUSED*" on
065     * its own line. The next one or more lines contain zero or more integers, each
066     * separated by any amount of whitespace (including newlines), identifying the
067     * IDs of blocks that were used to encrypt the message. These integers, in
068     * order, correspond to the <i>padsUsed</i> field of a HyperMessage.</li>
069     * <li>
070     * The <i>MAC</i> section starts with the header string "*MAC*" on its own line.
071     * The next line contains a 256-bit HEMAC-SHA256, encoded as a 64-character hex
072     * string. The next line contains eight encryption block IDs, separated by
073     * spaces; these are the eight blocks used in computation of the HEMAC-SHA1, as
074     * described in {@link HyperMAC}. If the message does not contain a MAC, the MAC
075     * section must still be present, but contains only whitespace.
076     * <li>
077     * The <i>subject</i> section starts with the header string "*SUBJECT*" on its
078     * own line. When parsing this section, leading and trailing whitespace is
079     * stripped, and newlines are replaced by spaces. This corresponds to the
080     * <i>subject</i> field of a HyperMessage.</li>
081     * <li>
082     * The <i>content</i> section starts with the header string "*CONTENT*" on its
083     * own line. When parsing this section, leading and trailing whitespace (other
084     * than newlines) is stripped; newlines are preserved. This corresponds to the
085     * <i>content</i> field of a HyperMessage.</li>
086     * </ul>
087     * 
088     * @see ContactList
089     * @see HyperMAC
090     */
091    public class EmailHyperCommunicator implements HyperCommunicator {
092      public static final String HYPERENC_HEADER = "X-Hyper-Encrypted";
093      
094      private static final Logger logger = Logger.getLogger(EmailHyperCommunicator.class);
095      
096      private Properties accountInfo;
097      private ContactList contactList;
098    
099      /**
100       * Creates a new EmailHyperCommunicator with the given account information.
101       * All e-mails sent from or received by this EmailHyperCommunicator use the
102       * account defined by this information.
103       * 
104       * @param accountInfo
105       *        a Properties object holding the account details for the e-mail
106       *        account this EmailHyperCommunicator should use.
107       * @param contactList
108       *        a list of Contacts; the EmailHyperCommunicator uses this list to
109       *        look up the Contact that corresponds to a given Address
110       */
111      public EmailHyperCommunicator(Properties accountInfo, ContactList contactList) {
112        this.accountInfo = accountInfo;
113        this.contactList = contactList;
114      }
115    
116      /**
117       * Send a HyperMessage via e-mail. Converts <code>hm</code> to a
118       * {@link javax.mail.Message} using <code>makeMessage</code>, then sends it
119       * over a {@link javax.mail.Transport} created using this
120       * EmailHyperCommunicator's account info.
121       * 
122       * @param hm
123       *        the HyperMessage to be sent
124       */
125      @Override
126      public void send(HyperMessage hm) {
127        Session mySession = Session.getDefaultInstance(accountInfo, null);
128        
129        String type = accountInfo.getProperty("outgoingType");
130        Transport t;
131        try {
132          t = mySession.getTransport(type);;
133        } catch (NoSuchProviderException e) {
134          throw new RuntimeException(
135              "Unable to create Transport for outgoing mail type " + type, e);
136        }
137        
138        // Attempt to make connection
139        String outgoingHost = accountInfo.getProperty("outgoingHost");
140        logger.info("Connecting to outgoing server " + outgoingHost);
141        try {
142          t.connect(outgoingHost,
143                    Integer.parseInt(accountInfo.getProperty("outgoingPort"), 10),
144                    accountInfo.getProperty("username"),
145                    accountInfo.getProperty("password")
146                    );
147        } catch (AuthenticationFailedException e) {
148          // TODO throw a more appropriate exception (preferably a checked one)
149          throw new IllegalStateException("Could not connect to server: authentication failed", e);
150        } catch (MessagingException e) {
151          throw new RuntimeException("Failed to connect to server", e);
152        }
153      
154        Message msg = makeMessage(hm, mySession);
155    
156        logger.info("Sending message (Subject: " + hm.getSubject() + ")");
157    
158        try {
159          t.sendMessage(msg, msg.getAllRecipients());
160        } catch (MessagingException e) {
161          throw new RuntimeException("Could not send message", e);
162        }
163        
164        try {
165          t.close();
166        } catch (MessagingException e) {
167          throw new RuntimeException("Error while closing connection to server", e);
168        }
169      }
170    
171      /**
172       * Converts an HyperMessage into a {@link javax.mail.Message} containing the
173       * same data. The returned Message is constructed using the account
174       * information from this Transmitter and is ready to be sent via e-mail.
175       * <p>
176       * The format of the returned Message is specified by the documentation for
177       * the EmailHyperCommunicator class.
178       * 
179       * @param hm
180       *        HyperMessage to be converted
181       * @param session
182       *        the {@link javax.mail.Session} containing mail properties used to
183       *        send this message
184       * @return Message containing the data from <code>hm</code>
185       */
186      protected Message makeMessage(HyperMessage hm, Session session) {
187        // Assemble body of e-mail message
188        String emailBody = "";
189        
190        emailBody += "*TYPE*\n";
191        emailBody += hm.getType().toString() + "\n\n";
192        
193        emailBody += "*PADSUSED*\n";
194        String idlist = "";
195        if (hm.getPadsUsed() != null) {
196          for (int id : hm.getPadsUsed()) {
197            idlist += id + " ";
198          }
199        }
200        emailBody += idlist.trim() + "\n\n";
201        
202        emailBody += "*MAC*\n";
203        String macidlist = "";
204        HyperMAC hemac = hm.getMac();
205        if (hemac != null) {
206          emailBody += HexCoder.encode(hemac.getMac()) + "\n";
207          for (int id : hemac.getBlockList()) {
208            macidlist += id + " ";
209          }
210          emailBody += macidlist.trim();
211        }
212        emailBody += "\n\n";
213        
214        emailBody += "*SUBJECT*\n";
215        emailBody += hm.getSubject() + "\n\n";
216        
217        emailBody += "*CONTENT*\n";
218        emailBody += hm.getContent() + "\n\n";
219        
220        Message msg = null;
221        try {
222          // Create message from parameters
223          msg = new MimeMessage(session);
224          
225          // Set message headers and body
226          msg.setFrom(hm.getSender().getEmail());
227          msg.setRecipient(Message.RecipientType.TO, hm.getRecipient().getEmail());
228          msg.setSentDate(hm.getDate());
229          msg.setSubject("Hyper-encrypted message [type: " + hm.getType().toString() + "]");
230          msg.setHeader(HYPERENC_HEADER, "text");
231          msg.setContent(emailBody, "text/plain");
232          msg.saveChanges();
233        } catch (MessagingException e) {
234          // it's really hard to know under what circumstances this exception
235          // is thrown, because EVERYTHING in the JavaMail package is declared to
236          // throw it with no explanation as to why...
237          // But we do expect the Message construction to succeed, and if it fails
238          // it's not clear that there's anything the client can do to recover
239          // anyway.
240          throw new RuntimeException("Unexpected MessagingException caught", e);
241        }
242        return msg;
243      }
244    
245      /**
246       * Retrieves incoming messages from the e-mail server. Converts incoming
247       * {@link javax.mail.Message}s to HyperMessages using
248       * <code>processMessage</code>.
249       * 
250       * @return a list of HyperMessages that have been received since the last time
251       *         this method was called
252       */
253      @Override
254      public List<HyperMessage> receive() {
255        Session mySession = Session.getDefaultInstance(System.getProperties(), null);
256        
257        String incomingType = accountInfo.getProperty("incomingType");
258        String host = accountInfo.getProperty("incomingHost");
259        String port = accountInfo.getProperty("incomingPort");
260        String username = accountInfo.getProperty("username");
261        String password = accountInfo.getProperty("password");
262    
263        Store store;
264        Folder inbox = null;
265        Message [] messages;
266        
267        // Attempt to make connection
268        try {
269          store = mySession.getStore(incomingType);
270        } catch (NoSuchProviderException e) {
271          throw new RuntimeException(
272              "Unable to create Store for incoming mail type " + incomingType, e);
273        }
274        
275        logger.info("Connecting to incoming e-mail server " + host);
276        
277        try {
278          store.connect(host, Integer.parseInt(port), username, password);
279        } catch (AuthenticationFailedException e) {
280          // TODO throw a more appropriate exception (preferably a checked one)
281          throw new IllegalStateException("Could not connect to server: authentication failed", e);
282        } catch (MessagingException e) {
283          throw new RuntimeException("Failed to connect to server", e);
284        }
285        
286        try {
287          inbox = store.getFolder("INBOX");
288          inbox.open(Folder.READ_WRITE);
289          messages = inbox.getMessages();
290        } catch (MessagingException e) {
291          throw new RuntimeException("Failed to read messages from INBOX", e);
292        }
293    
294        List<HyperMessage> incomingMessages = new ArrayList<HyperMessage>();
295        for (Message m : messages) {
296          HyperMessage hm;
297          try {
298            hm = processMessage(m);
299          } catch (MessageParseException e) {
300            // discard the message and move on
301            // XXX probably there should be a better solution here
302            continue;
303          }
304          
305          if (hm != null) {
306            try {
307              m.setFlag(Flags.Flag.DELETED, true);
308            } catch (MessagingException e) {
309              throw new RuntimeException("Error while deleting message", e);
310            }
311            incomingMessages.add(hm);
312          }
313        }
314        
315        logger.info("Received " + incomingMessages.size() + " messages");
316        
317        // Clear out messages marked for deletion above
318        // TODO should these really be expunged now? if there is a failure before
319        // these messages are stored locally, we will lose them...
320        try {
321          logger.info("Expunging downloaded messages from server");
322          inbox.expunge();
323        } catch (MessagingException e) {
324          throw new RuntimeException("Error while expunging mail", e);
325        }
326        
327        return incomingMessages;
328      }
329    
330      /**
331       * Create a HyperMessage from the given Message. If the given Message is not a
332       * hyper-encryption protocol message or a hyper-encrypted message, returns
333       * <code>null</code>. If <code>msg</code> does not have a received date
334       * (obtained via <code>getReceivedDate()</code>), the current time is used.
335       * <p>
336       * The recipient and sender Address of the message are converted to Contacts
337       * using the <code>contactMap</code> with which this HyperCommunicator was
338       * constructed.
339       * <p>
340       * The format expected when parsing the given Message is specified by the
341       * documentation for the EmailHyperCommunicator class.
342       * 
343       * @param msg
344       *        The received Message
345       * @return a HyperMessage built using the data parsed from <code>msg</code>,
346       *         or <code>null</code> if <code>msg</code> is not a hyper-encryption
347       *         message
348       * @throws MessageParseException
349       *         if the body of <code>msg</code> could not be parsed
350       */
351      protected HyperMessage processMessage(Message msg)
352          throws MessageParseException {
353        String bodyFormat;
354        String typeStr = "";
355        String padList = "";
356        String subject = "";
357        String content = "";
358        String mac = "";
359        
360        Contact fromContact;
361        Contact toContact;
362        Date msgDate;
363        
364        String emailBody;
365        
366        // TODO make the scope of this try block more narrow
367        try {
368          // Check for the header indicating the format of the body of this message
369          bodyFormat = msg.getHeader(HYPERENC_HEADER)[0];
370          
371          // If no such header exists, this isn't a hyper-encrypted message
372          if (bodyFormat == null) {
373            return null;
374          }
375          
376          // Tokenize body for analysis
377          emailBody = (String) msg.getContent();  // XXX make sure this cast is OK
378          String lines[] = emailBody.split("\\r?\\n");
379    
380          String lastHeader = "";
381          for (String line : lines) {
382            line = line.trim();
383            if (line.equals("*TYPE*") || line.equals("*PADSUSED*") ||
384                line.equals("*SUBJECT*") || line.equals("*CONTENT*") ||
385                line.equals("*MAC*")) {
386              lastHeader = line.trim();
387            } else {
388              if (lastHeader.equals("*TYPE*")) {
389                typeStr += line + " ";
390              } else if (lastHeader.equals("*PADSUSED*")) {
391                padList += line + " ";
392              } else if (lastHeader.equals("*SUBJECT*")) {
393                subject += line + " ";
394              } else if (lastHeader.equals("*CONTENT*")) {
395                // Use newline instead of space, because Content probably is
396                // supposed to be a multi-line field
397                content += line + "\n";
398              } else if (lastHeader.equals("*MAC*")) {
399                mac += line + " ";
400              }
401            }
402          }
403          
404          // Strip any trailing whitespace introduced by our parsing above
405          typeStr = typeStr.trim();
406          padList = padList.trim();
407          subject = subject.trim();
408          content = content.trim();
409          mac = mac.trim();
410          
411          fromContact = contactList.getContact(msg.getFrom()[0]);
412          toContact = contactList.getContact(msg.getRecipients(RecipientType.TO)[0]);
413          
414          msgDate = msg.getReceivedDate();
415          if (msgDate == null)
416            msgDate = new Date();   // TODO use fake system clock
417        } catch (MessagingException e) {
418          throw new RuntimeException(
419              "Unexpected MessagingException caught while processing message", e);
420        } catch (IOException e) {
421          throw new RuntimeException(
422              "Unexpected IOException caught while processing message", e);
423        }
424        
425        List<Integer> blocksUsed = new ArrayList<Integer>();
426        if (padList.trim().isEmpty()) {
427          blocksUsed = null;
428        } else {
429          String padIds[] = padList.split("\\s+");  // split on any whitespace
430          for (String s : padIds) {
431            if (s.isEmpty())
432              continue;
433            
434            try {
435              blocksUsed.add(Integer.parseInt(s));
436            } catch (NumberFormatException e) {
437              throw new MessageParseException(
438                  "Could not parse pad block ID", emailBody, e);
439            }
440          }
441        }
442        
443        HyperMessageType type = HyperMessageType.fromString(typeStr);
444        
445        if (type == null)
446          throw new MessageParseException(
447              "Unknown message type " + typeStr, emailBody);
448        
449        
450        HyperMAC hemac;
451        if (mac.isEmpty()) {
452          hemac = null;
453        } else {
454          List<Integer> macBlocksUsed = new ArrayList<Integer>();
455          byte[] macBytes;
456          
457          String blockIds[] = mac.split("\\s+");  // split on any whitespace
458          String val = null;
459          for (String s : blockIds) {
460            if (s.isEmpty())
461              continue;
462            
463            if (val == null) {
464              val = s;
465              continue;
466            }
467            
468            try {
469              macBlocksUsed.add(Integer.parseInt(s));
470            } catch (NumberFormatException e) {
471              throw new MessageParseException(
472                  "Could not parse pad block ID", emailBody, e);
473            }
474          }
475          
476          try {
477            macBytes = HexCoder.decode(val);
478          } catch (IllegalArgumentException e) {
479            throw new MessageParseException(
480                "Illegal MAC value " + val, emailBody, e);
481          }
482          
483          hemac = new HyperMAC(macBlocksUsed, macBytes);
484        }
485        
486        
487        HyperMessage hm = HyperMessage.getInstance(
488            type,
489            fromContact,
490            toContact,
491            subject,
492            content,
493            blocksUsed,
494            hemac,
495            msgDate
496            );
497        return hm;
498        
499        /*
500        } else if (subject.equals(Contacter.HYPER_SETUP_SUBJECT)) {
501          System.out.println("Setup request");
502          myContacter.recvContact(fromAddress, content);
503          return null;  // XXX what needs to be returned here? anything?
504        }
505        */
506      } 
507      
508    }