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 }